Merge branch 'develop' into develop
This commit is contained in:
commit
5eb2e71f0f
1
.github/helper/.flake8_strict
vendored
1
.github/helper/.flake8_strict
vendored
@ -66,6 +66,7 @@ ignore =
|
||||
F841,
|
||||
E713,
|
||||
E712,
|
||||
B023
|
||||
|
||||
|
||||
max-line-length = 200
|
||||
|
2
.github/workflows/docs-checker.yml
vendored
2
.github/workflows/docs-checker.yml
vendored
@ -12,7 +12,7 @@ jobs:
|
||||
- name: 'Setup Environment'
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: 'Clone repo'
|
||||
uses: actions/checkout@v2
|
||||
|
16
.github/workflows/linters.yml
vendored
16
.github/workflows/linters.yml
vendored
@ -11,10 +11,10 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Set up Python 3.8
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.3
|
||||
@ -22,10 +22,8 @@ jobs:
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
- name: Download semgrep
|
||||
run: pip install semgrep==0.97.0
|
||||
|
||||
- name: Run Semgrep rules
|
||||
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
|
||||
|
25
.github/workflows/patch.yml
vendored
25
.github/workflows/patch.yml
vendored
@ -35,9 +35,9 @@ jobs:
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
uses: "gabrielfalcao/pyenv-action@v9"
|
||||
with:
|
||||
python-version: 3.8
|
||||
versions: 3.10:latest, 3.7:latest
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
@ -52,7 +52,7 @@ jobs:
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
@ -82,7 +82,10 @@ jobs:
|
||||
${{ runner.os }}-yarn-
|
||||
|
||||
- name: Install
|
||||
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
run: |
|
||||
pip install frappe-bench
|
||||
pyenv global $(pyenv versions | grep '3.10')
|
||||
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
|
||||
env:
|
||||
DB: mariadb
|
||||
TYPE: server
|
||||
@ -96,18 +99,23 @@ jobs:
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
|
||||
pyenv global $(pyenv versions | grep '3.7')
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version-hotfix"
|
||||
|
||||
|
||||
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
|
||||
git -C "apps/frappe" checkout -q -f $branch_name
|
||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||
|
||||
bench setup requirements --python
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench setup env
|
||||
bench pip install -e ./apps/erpnext
|
||||
|
||||
bench --site test_site migrate
|
||||
done
|
||||
|
||||
@ -115,5 +123,10 @@ jobs:
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
bench setup requirements --python
|
||||
|
||||
pyenv global $(pyenv versions | grep '3.10')
|
||||
rm -rf ~/frappe-bench/env
|
||||
bench -v setup env
|
||||
bench pip install -e ./apps/erpnext
|
||||
|
||||
bench --site test_site migrate
|
||||
|
6
.github/workflows/server-tests-mariadb.yml
vendored
6
.github/workflows/server-tests-mariadb.yml
vendored
@ -39,7 +39,7 @@ jobs:
|
||||
fail-fast: false
|
||||
|
||||
matrix:
|
||||
container: [1, 2, 3]
|
||||
container: [1, 2, 3, 4]
|
||||
|
||||
name: Python Unit Tests
|
||||
|
||||
@ -59,7 +59,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
@ -74,7 +74,7 @@ jobs:
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
6
.github/workflows/server-tests-postgres.yml
vendored
6
.github/workflows/server-tests-postgres.yml
vendored
@ -21,7 +21,7 @@ jobs:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
container: [1, 2, 3]
|
||||
container: [1]
|
||||
|
||||
name: Python Unit Tests
|
||||
|
||||
@ -46,7 +46,7 @@ jobs:
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v2
|
||||
@ -61,7 +61,7 @@ jobs:
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
|
||||
key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-pip-
|
||||
${{ runner.os }}-
|
||||
|
33
CODEOWNERS
33
CODEOWNERS
@ -3,33 +3,30 @@
|
||||
# These owners will be the default owners for everything in
|
||||
# the repo. Unless a later match takes precedence,
|
||||
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/assets/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/erpnext_integrations/ @nextchamp-saqib
|
||||
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007
|
||||
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
|
||||
erpnext/support/ @nextchamp-saqib @deepeshgarg007
|
||||
pos* @nextchamp-saqib
|
||||
|
||||
erpnext/buying/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/e_commerce/ @marination
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure
|
||||
erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/portal/ @marination
|
||||
erpnext/quality_management/ @marination @rohitwaghchaure
|
||||
erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
|
||||
erpnext/shopping_cart/ @marination
|
||||
erpnext/stock/ @marination @rohitwaghchaure @ankush
|
||||
erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
|
||||
|
||||
erpnext/crm/ @ruchamahabal @pateljannat
|
||||
erpnext/education/ @ruchamahabal @pateljannat
|
||||
erpnext/hr/ @ruchamahabal @pateljannat
|
||||
erpnext/payroll @ruchamahabal @pateljannat
|
||||
erpnext/projects/ @ruchamahabal @pateljannat
|
||||
erpnext/crm/ @NagariaHussain
|
||||
erpnext/education/ @rutwikhdev
|
||||
erpnext/projects/ @ruchamahabal
|
||||
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination @ankush
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination @ankush
|
||||
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
|
||||
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination
|
||||
erpnext/public/ @nextchamp-saqib @marination
|
||||
|
||||
.github/ @ankush
|
||||
requirements.txt @gavindsouza
|
||||
pyproject.toml @gavindsouza @ankush
|
||||
|
@ -1 +0,0 @@
|
||||
hypothesis~=6.31.0
|
@ -11,3 +11,41 @@ Entries are:
|
||||
- Purchase Invoice (Itemised)
|
||||
|
||||
All accounting entries are stored in the `General Ledger`
|
||||
|
||||
## Payment Ledger
|
||||
Transactions on Receivable and Payable Account types will also be stored in `Payment Ledger`. This is so that payment reconciliation process only requires update on this ledger.
|
||||
|
||||
### Key Fields
|
||||
| Field | Description |
|
||||
|----------------------|----------------------------------|
|
||||
| `account_type` | Receivable/Payable |
|
||||
| `account` | Accounting head |
|
||||
| `party` | Party Name |
|
||||
| `voucher_no` | Voucher No |
|
||||
| `against_voucher_no` | Linked voucher(secondary effect) |
|
||||
| `amount` | can be +ve/-ve |
|
||||
|
||||
### Design
|
||||
`debit` and `credit` have been replaced with `account_type` and `amount`. `against_voucher_no` is populated for all entries. So, outstanding amount can be calculated by summing up amount only using `against_voucher_no`.
|
||||
|
||||
Ex:
|
||||
1. Consider an invoice for ₹100 and a partial payment of ₹80 against that invoice. Payment Ledger will have following entries.
|
||||
|
||||
| voucher_no | against_voucher_no | amount |
|
||||
|------------|--------------------|--------|
|
||||
| SINV-01 | SINV-01 | 100 |
|
||||
| PAY-01 | SINV-01 | -80 |
|
||||
|
||||
|
||||
2. Reconcile a Credit Note against an invoice using a Journal Entry
|
||||
|
||||
An invoice for ₹100 partially reconciled against a credit of ₹70 using a Journal Entry. Payment Ledger will have the following entries.
|
||||
|
||||
| voucher_no | against_voucher_no | amount |
|
||||
|------------|--------------------|--------|
|
||||
| SINV-01 | SINV-01 | 100 |
|
||||
| | | |
|
||||
| CR-NOTE-01 | CR-NOTE-01 | -70 |
|
||||
| | | |
|
||||
| JE-01 | CR-NOTE-01 | +70 |
|
||||
| JE-01 | SINV-01 | -70 |
|
||||
|
@ -1 +0,0 @@
|
||||
C Form (India specific only) - Will be deprecated.
|
@ -1,43 +0,0 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
//c-form js file
|
||||
// -----------------------------
|
||||
|
||||
frappe.ui.form.on('C-Form', {
|
||||
setup(frm) {
|
||||
frm.fields_dict.invoices.grid.get_field("invoice_no").get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"docstatus": 1,
|
||||
"customer": doc.customer,
|
||||
"company": doc.company,
|
||||
"c_form_applicable": 'Yes',
|
||||
"c_form_no": ''
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
frm.fields_dict.state.get_query = function() {
|
||||
return {
|
||||
filters: {
|
||||
country: "India"
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on('C-Form Invoice Detail', {
|
||||
invoice_no(frm, cdt, cdn) {
|
||||
let d = frappe.get_doc(cdt, cdn);
|
||||
|
||||
if (d.invoice_no) {
|
||||
frm.call('get_invoice_details', {
|
||||
invoice_no: d.invoice_no
|
||||
}).then(r => {
|
||||
frappe.model.set_value(cdt, cdn, r.message);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -1,511 +0,0 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 0,
|
||||
"autoname": "naming_series:",
|
||||
"beta": 0,
|
||||
"creation": "2013-03-07 11:55:06",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 0,
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "",
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Series",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "ACC-CF-.YYYY.-",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "c_form_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "C-Form No",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "received_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Received Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "customer",
|
||||
"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": "Customer",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Customer",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break1",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "50%",
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0,
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "quarter",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Quarter",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "\nI\nII\nIII\nIV",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "total_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Total Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "State",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break0",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "invoices",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Invoices",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "C-Form Invoice Detail",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "total_invoiced_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Total Invoiced Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "amended_from",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 1,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Amended From",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "C-Form",
|
||||
"permlevel": 0,
|
||||
"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
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 1,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 3,
|
||||
"modified": "2018-08-21 14:44:30.558767",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "C-Form",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Accounts Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 0,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 1,
|
||||
"print": 0,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "All",
|
||||
"set_user_permissions": 0,
|
||||
"share": 0,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_order": "DESC",
|
||||
"timeline_field": "customer",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
}
|
@ -1,96 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import flt
|
||||
|
||||
|
||||
class CForm(Document):
|
||||
def validate(self):
|
||||
"""Validate invoice that c-form is applicable
|
||||
and no other c-form is received for that"""
|
||||
|
||||
for d in self.get("invoices"):
|
||||
if d.invoice_no:
|
||||
inv = frappe.db.sql(
|
||||
"""select c_form_applicable, c_form_no from
|
||||
`tabSales Invoice` where name = %s and docstatus = 1""",
|
||||
d.invoice_no,
|
||||
)
|
||||
|
||||
if inv and inv[0][0] != "Yes":
|
||||
frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no))
|
||||
|
||||
elif inv and inv[0][1] and inv[0][1] != self.name:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""Invoice {0} is tagged in another C-form: {1}.
|
||||
If you want to change C-form no for this invoice,
|
||||
please remove invoice no from the previous c-form and then try again""".format(
|
||||
d.invoice_no, inv[0][1]
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
elif not inv:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
|
||||
Please enter a valid Invoice".format(
|
||||
d.idx, d.invoice_no
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
def on_update(self):
|
||||
"""Update C-Form No on invoices"""
|
||||
self.set_total_invoiced_amount()
|
||||
|
||||
def on_submit(self):
|
||||
self.set_cform_in_sales_invoices()
|
||||
|
||||
def before_cancel(self):
|
||||
# remove cform reference
|
||||
frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name)
|
||||
|
||||
def set_cform_in_sales_invoices(self):
|
||||
inv = [d.invoice_no for d in self.get("invoices")]
|
||||
if inv:
|
||||
frappe.db.sql(
|
||||
"""update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)"""
|
||||
% ("%s", "%s", ", ".join(["%s"] * len(inv))),
|
||||
tuple([self.name, self.modified] + inv),
|
||||
)
|
||||
|
||||
frappe.db.sql(
|
||||
"""update `tabSales Invoice` set c_form_no = null, modified = %s
|
||||
where name not in (%s) and ifnull(c_form_no, '') = %s"""
|
||||
% ("%s", ", ".join(["%s"] * len(inv)), "%s"),
|
||||
tuple([self.modified] + inv + [self.name]),
|
||||
)
|
||||
else:
|
||||
frappe.throw(_("Please enter atleast 1 invoice in the table"))
|
||||
|
||||
def set_total_invoiced_amount(self):
|
||||
total = sum(flt(d.grand_total) for d in self.get("invoices"))
|
||||
frappe.db.set(self, "total_invoiced_amount", total)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_invoice_details(self, invoice_no):
|
||||
"""Pull details from invoices for referrence"""
|
||||
if invoice_no:
|
||||
inv = frappe.db.get_value(
|
||||
"Sales Invoice",
|
||||
invoice_no,
|
||||
["posting_date", "territory", "base_net_total", "base_grand_total"],
|
||||
as_dict=True,
|
||||
)
|
||||
return {
|
||||
"invoice_date": inv.posting_date,
|
||||
"territory": inv.territory,
|
||||
"net_total": inv.base_net_total,
|
||||
"grand_total": inv.base_grand_total,
|
||||
}
|
@ -1,10 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import unittest
|
||||
|
||||
# test_records = frappe.get_test_records('C-Form')
|
||||
|
||||
|
||||
class TestCForm(unittest.TestCase):
|
||||
pass
|
@ -1 +0,0 @@
|
||||
Invoice detail for parent C-Form.
|
@ -1,168 +0,0 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-02-22 01:27:38",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "invoice_no",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice No",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Sales Invoice",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "160px",
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"width": "160px"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "invoice_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Invoice Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"width": "120px"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"description": "",
|
||||
"fieldname": "territory",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Territory",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Territory",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"width": "120px"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "net_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Net Total",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"width": "120px"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "grand_total",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Grand Total",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company:company:default_currency",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"print_width": "120px",
|
||||
"read_only": 1,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0,
|
||||
"width": "120px"
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-07-11 03:27:58.768719",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "C-Form Invoice Detail",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"track_seen": 0
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class CFormInvoiceDetail(Document):
|
||||
pass
|
@ -29,7 +29,6 @@ def test_create_test_data():
|
||||
"item_name": "_Test Tesla Car",
|
||||
"apply_warehouse_wise_reorder_level": 0,
|
||||
"warehouse": "Stores - _TC",
|
||||
"gst_hsn_code": "999800",
|
||||
"valuation_rate": 5000,
|
||||
"standard_rate": 5000,
|
||||
"item_defaults": [
|
||||
|
@ -58,16 +58,20 @@ class GLEntry(Document):
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
)
|
||||
if frappe.db.get_value("Account", self.account, "account_type") not in [
|
||||
"Receivable",
|
||||
"Payable",
|
||||
]:
|
||||
# Update outstanding amt on against voucher
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.against_voucher
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_outstanding_amt(
|
||||
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
|
||||
)
|
||||
|
||||
def check_mandatory(self):
|
||||
mandatory = ["account", "voucher_type", "voucher_no", "company"]
|
||||
|
@ -1,90 +0,0 @@
|
||||
{
|
||||
"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",
|
||||
"utgst_account",
|
||||
"is_reverse_charge_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "cgst_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "CGST Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "sgst_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "SGST Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "igst_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "IGST Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldname": "utgst_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "UTGST Account",
|
||||
"options": "Account"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-07 12:59:14.039768",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class GSTAccount(Document):
|
||||
pass
|
@ -149,22 +149,6 @@ frappe.ui.form.on("Journal Entry", {
|
||||
}
|
||||
});
|
||||
}
|
||||
else if(frm.doc.voucher_type=="Opening Entry") {
|
||||
return frappe.call({
|
||||
type:"GET",
|
||||
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
|
||||
args: {
|
||||
"company": frm.doc.company
|
||||
},
|
||||
callback: function(r) {
|
||||
frappe.model.clear_table(frm.doc, "accounts");
|
||||
if(r.message) {
|
||||
update_jv_details(frm.doc, r.message);
|
||||
}
|
||||
cur_frm.set_value("is_opening", "Yes");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -137,7 +137,8 @@
|
||||
"fieldname": "finance_book",
|
||||
"fieldtype": "Link",
|
||||
"label": "Finance Book",
|
||||
"options": "Finance Book"
|
||||
"options": "Finance Book",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "2_add_edit_gl_entries",
|
||||
@ -538,7 +539,7 @@
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-06 17:18:46.865259",
|
||||
"modified": "2022-06-23 22:01:32.348337",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
|
@ -416,7 +416,7 @@ class JournalEntry(AccountsController):
|
||||
against_entries = frappe.db.sql(
|
||||
"""select * from `tabJournal Entry Account`
|
||||
where account = %s and docstatus = 1 and parent = %s
|
||||
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
|
||||
and (reference_type is null or reference_type in ('', 'Sales Order', 'Purchase Order'))
|
||||
""",
|
||||
(d.account, d.reference_name),
|
||||
as_dict=True,
|
||||
@ -800,9 +800,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
self.total_amount_in_words = money_in_words(amt, currency)
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
def build_gl_map(self):
|
||||
gl_map = []
|
||||
for d in self.get("accounts"):
|
||||
if d.debit or d.credit:
|
||||
@ -838,7 +836,12 @@ class JournalEntry(AccountsController):
|
||||
item=d,
|
||||
)
|
||||
)
|
||||
return gl_map
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
from erpnext.accounts.general_ledger import make_gl_entries
|
||||
|
||||
gl_map = self.build_gl_map()
|
||||
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
|
||||
update_outstanding = "No"
|
||||
else:
|
||||
@ -1201,24 +1204,6 @@ def get_payment_entry(ref_doc, args):
|
||||
return je if args.get("journal_entry") else je.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opening_accounts(company):
|
||||
"""get all balance sheet accounts for opening entry"""
|
||||
accounts = frappe.db.sql_list(
|
||||
"""select
|
||||
name from tabAccount
|
||||
where
|
||||
is_group=0 and report_type='Balance Sheet' and company={0} and
|
||||
name not in (select distinct account from tabWarehouse where
|
||||
account is not null and account != '')
|
||||
order by name asc""".format(
|
||||
frappe.db.escape(company)
|
||||
)
|
||||
)
|
||||
|
||||
return [{"account": a, "balance": get_balance_on(a)} for a in accounts]
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
@ -1,17 +0,0 @@
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
refresh: function(frm) {
|
||||
frm.set_query('company_address', function(doc) {
|
||||
if(!doc.company) {
|
||||
frappe.throw(__('Please set Company'));
|
||||
}
|
||||
|
||||
return {
|
||||
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||
filters: {
|
||||
link_doctype: 'Company',
|
||||
link_name: doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
@ -6,7 +6,7 @@ import json
|
||||
from functools import reduce
|
||||
|
||||
import frappe
|
||||
from frappe import ValidationError, _, scrub, throw
|
||||
from frappe import ValidationError, _, qb, scrub, throw
|
||||
from frappe.utils import cint, comma_or, flt, getdate, nowdate
|
||||
|
||||
import erpnext
|
||||
@ -785,7 +785,7 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
self.set("remarks", "\n".join(remarks))
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
def build_gl_map(self):
|
||||
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
|
||||
self.setup_party_account_field()
|
||||
|
||||
@ -794,7 +794,10 @@ class PaymentEntry(AccountsController):
|
||||
self.add_bank_gl_entries(gl_entries)
|
||||
self.add_deductions_gl_entries(gl_entries)
|
||||
self.add_tax_gl_entries(gl_entries)
|
||||
return gl_entries
|
||||
|
||||
def make_gl_entries(self, cancel=0, adv_adj=0):
|
||||
gl_entries = self.build_gl_map()
|
||||
gl_entries = process_gl_map(gl_entries)
|
||||
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
|
||||
|
||||
@ -1195,6 +1198,9 @@ def get_outstanding_reference_documents(args):
|
||||
if args.get("party_type") == "Member":
|
||||
return
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
common_filter = []
|
||||
|
||||
# confirm that Supplier is not blocked
|
||||
if args.get("party_type") == "Supplier":
|
||||
supplier_status = get_supplier_block_status(args["party"])
|
||||
@ -1216,10 +1222,13 @@ def get_outstanding_reference_documents(args):
|
||||
condition = " and voucher_type={0} and voucher_no={1}".format(
|
||||
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
|
||||
)
|
||||
common_filter.append(ple.voucher_type == args["voucher_type"])
|
||||
common_filter.append(ple.voucher_no == args["voucher_no"])
|
||||
|
||||
# Add cost center condition
|
||||
if args.get("cost_center"):
|
||||
condition += " and cost_center='%s'" % args.get("cost_center")
|
||||
common_filter.append(ple.cost_center == args.get("cost_center"))
|
||||
|
||||
date_fields_dict = {
|
||||
"posting_date": ["from_posting_date", "to_posting_date"],
|
||||
@ -1231,16 +1240,19 @@ def get_outstanding_reference_documents(args):
|
||||
condition += " and {0} between '{1}' and '{2}'".format(
|
||||
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
|
||||
)
|
||||
common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
|
||||
|
||||
if args.get("company"):
|
||||
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
|
||||
common_filter.append(ple.company == args.get("company"))
|
||||
|
||||
outstanding_invoices = get_outstanding_invoices(
|
||||
args.get("party_type"),
|
||||
args.get("party"),
|
||||
args.get("party_account"),
|
||||
filters=args,
|
||||
condition=condition,
|
||||
common_filter=common_filter,
|
||||
min_outstanding=args.get("outstanding_amt_greater_than"),
|
||||
max_outstanding=args.get("outstanding_amt_less_than"),
|
||||
)
|
||||
|
||||
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
|
||||
|
@ -1,29 +0,0 @@
|
||||
frappe.ui.form.on("Payment Entry", {
|
||||
company: function(frm) {
|
||||
frappe.call({
|
||||
'method': 'frappe.contacts.doctype.address.address.get_default_address',
|
||||
'args': {
|
||||
'doctype': 'Company',
|
||||
'name': frm.doc.company
|
||||
},
|
||||
'callback': function(r) {
|
||||
frm.set_value('company_address', r.message);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
party: function(frm) {
|
||||
if (frm.doc.party_type == "Customer" && frm.doc.party) {
|
||||
frappe.call({
|
||||
'method': 'frappe.contacts.doctype.address.address.get_default_address',
|
||||
'args': {
|
||||
'doctype': 'Customer',
|
||||
'name': frm.doc.party
|
||||
},
|
||||
'callback': function(r) {
|
||||
frm.set_value('customer_address', r.message);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -4,6 +4,7 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import flt, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import (
|
||||
@ -24,7 +25,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
||||
test_dependencies = ["Item"]
|
||||
|
||||
|
||||
class TestPaymentEntry(unittest.TestCase):
|
||||
class TestPaymentEntry(FrappeTestCase):
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_payment_entry_against_order(self):
|
||||
so = make_sales_order()
|
||||
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
|
||||
|
@ -6,6 +6,19 @@ import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_checks_for_pl_and_bs_accounts,
|
||||
)
|
||||
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
|
||||
get_dimension_filter_map,
|
||||
)
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import (
|
||||
validate_balance_type,
|
||||
validate_frozen_account,
|
||||
)
|
||||
from erpnext.accounts.utils import update_voucher_outstanding
|
||||
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
|
||||
|
||||
|
||||
class PaymentLedgerEntry(Document):
|
||||
def validate_account(self):
|
||||
@ -18,5 +31,119 @@ class PaymentLedgerEntry(Document):
|
||||
if not valid_account:
|
||||
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
|
||||
|
||||
def validate_account_details(self):
|
||||
"""Account must be ledger, active and not freezed"""
|
||||
|
||||
ret = frappe.db.sql(
|
||||
"""select is_group, docstatus, company
|
||||
from tabAccount where name=%s""",
|
||||
self.account,
|
||||
as_dict=1,
|
||||
)[0]
|
||||
|
||||
if ret.is_group == 1:
|
||||
frappe.throw(
|
||||
_(
|
||||
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
|
||||
).format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.docstatus == 2:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
|
||||
)
|
||||
|
||||
if ret.company != self.company:
|
||||
frappe.throw(
|
||||
_("{0} {1}: Account {2} does not belong to Company {3}").format(
|
||||
self.voucher_type, self.voucher_no, self.account, self.company
|
||||
)
|
||||
)
|
||||
|
||||
def validate_allowed_dimensions(self):
|
||||
dimension_filter_map = get_dimension_filter_map()
|
||||
for key, value in dimension_filter_map.items():
|
||||
dimension = key[0]
|
||||
account = key[1]
|
||||
|
||||
if self.account == account:
|
||||
if value["is_mandatory"] and not self.get(dimension):
|
||||
frappe.throw(
|
||||
_("{0} is mandatory for account {1}").format(
|
||||
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
|
||||
),
|
||||
MandatoryAccountDimensionError,
|
||||
)
|
||||
|
||||
if value["allow_or_restrict"] == "Allow":
|
||||
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
else:
|
||||
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
|
||||
frappe.throw(
|
||||
_("Invalid value {0} for {1} against account {2}").format(
|
||||
frappe.bold(self.get(dimension)),
|
||||
frappe.bold(frappe.unscrub(dimension)),
|
||||
frappe.bold(self.account),
|
||||
),
|
||||
InvalidAccountDimensionError,
|
||||
)
|
||||
|
||||
def validate_dimensions_for_pl_and_bs(self):
|
||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
||||
|
||||
for dimension in get_checks_for_pl_and_bs_accounts():
|
||||
if (
|
||||
account_type == "Profit and Loss"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_pl
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
)
|
||||
|
||||
if (
|
||||
account_type == "Balance Sheet"
|
||||
and self.company == dimension.company
|
||||
and dimension.mandatory_for_bs
|
||||
and not dimension.disabled
|
||||
):
|
||||
if not self.get(dimension.fieldname):
|
||||
frappe.throw(
|
||||
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
|
||||
dimension.label, self.account
|
||||
)
|
||||
)
|
||||
|
||||
def validate(self):
|
||||
self.validate_account()
|
||||
|
||||
def on_update(self):
|
||||
adv_adj = self.flags.adv_adj
|
||||
if not self.flags.from_repost:
|
||||
self.validate_account_details()
|
||||
self.validate_dimensions_for_pl_and_bs()
|
||||
self.validate_allowed_dimensions()
|
||||
validate_balance_type(self.account, adv_adj)
|
||||
validate_frozen_account(self.account, adv_adj)
|
||||
|
||||
# update outstanding amount
|
||||
if (
|
||||
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
|
||||
and self.flags.update_outstanding == "Yes"
|
||||
and not frappe.flags.is_reverse_depr_entry
|
||||
):
|
||||
update_voucher_outstanding(
|
||||
self.against_voucher_type, self.against_voucher_no, self.account, self.party_type, self.party
|
||||
)
|
||||
|
@ -3,16 +3,26 @@
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe import _, msgprint, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.custom import ConstantColumn
|
||||
from frappe.query_builder.functions import IfNull
|
||||
from frappe.utils import flt, getdate, nowdate, today
|
||||
|
||||
import erpnext
|
||||
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
|
||||
from erpnext.accounts.utils import (
|
||||
QueryPaymentLedger,
|
||||
get_outstanding_invoices,
|
||||
reconcile_against_document,
|
||||
)
|
||||
from erpnext.controllers.accounts_controller import get_advance_payment_entries
|
||||
|
||||
|
||||
class PaymentReconciliation(Document):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(PaymentReconciliation, self).__init__(*args, **kwargs)
|
||||
self.common_filter_conditions = []
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_unreconciled_entries(self):
|
||||
self.get_nonreconciled_payment_entries()
|
||||
@ -108,54 +118,58 @@ class PaymentReconciliation(Document):
|
||||
return list(journal_entries)
|
||||
|
||||
def get_dr_or_cr_notes(self):
|
||||
condition = self.get_conditions(get_return_invoices=True)
|
||||
dr_or_cr = (
|
||||
"credit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "debit_in_account_currency"
|
||||
)
|
||||
|
||||
reconciled_dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if dr_or_cr == "credit_in_account_currency"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
self.build_qb_filter_conditions(get_return_invoices=True)
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
|
||||
|
||||
return frappe.db.sql(
|
||||
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
|
||||
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
|
||||
account_currency as currency
|
||||
FROM `tab{doc}` doc, `tabGL Entry` gl
|
||||
WHERE
|
||||
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
|
||||
and doc.{party_type_field} = %(party)s
|
||||
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
|
||||
and gl.against_voucher_type = %(voucher_type)s
|
||||
and doc.docstatus = 1 and gl.party = %(party)s
|
||||
and gl.party_type = %(party_type)s and gl.account = %(account)s
|
||||
and gl.is_cancelled = 0 {condition}
|
||||
GROUP BY doc.name
|
||||
Having
|
||||
amount > 0
|
||||
ORDER BY doc.posting_date
|
||||
""".format(
|
||||
doc=voucher_type,
|
||||
dr_or_cr=dr_or_cr,
|
||||
reconciled_dr_or_cr=reconciled_dr_or_cr,
|
||||
party_type_field=frappe.scrub(self.party_type),
|
||||
condition=condition or "",
|
||||
),
|
||||
{
|
||||
"party": self.party,
|
||||
"party_type": self.party_type,
|
||||
"voucher_type": voucher_type,
|
||||
"account": self.receivable_payable_account,
|
||||
},
|
||||
as_dict=1,
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable":
|
||||
self.common_filter_conditions.append(ple.account_type == "Receivable")
|
||||
else:
|
||||
self.common_filter_conditions.append(ple.account_type == "Payable")
|
||||
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
|
||||
|
||||
# get return invoices
|
||||
doc = qb.DocType(voucher_type)
|
||||
return_invoices = (
|
||||
qb.from_(doc)
|
||||
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
|
||||
.where(
|
||||
(doc.docstatus == 1)
|
||||
& (doc[frappe.scrub(self.party_type)] == self.party)
|
||||
& (doc.is_return == 1)
|
||||
& (IfNull(doc.return_against, "") == "")
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
|
||||
outstanding_dr_or_cr = []
|
||||
if return_invoices:
|
||||
ple_query = QueryPaymentLedger()
|
||||
return_outstanding = ple_query.get_voucher_outstandings(
|
||||
vouchers=return_invoices,
|
||||
common_filter=self.common_filter_conditions,
|
||||
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
|
||||
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
|
||||
get_payments=True,
|
||||
)
|
||||
|
||||
for inv in return_outstanding:
|
||||
if inv.outstanding != 0:
|
||||
outstanding_dr_or_cr.append(
|
||||
frappe._dict(
|
||||
{
|
||||
"reference_type": inv.voucher_type,
|
||||
"reference_name": inv.voucher_no,
|
||||
"amount": -(inv.outstanding_in_account_currency),
|
||||
"posting_date": inv.posting_date,
|
||||
"currency": inv.currency,
|
||||
}
|
||||
)
|
||||
)
|
||||
return outstanding_dr_or_cr
|
||||
|
||||
def add_payment_entries(self, non_reconciled_payments):
|
||||
self.set("payments", [])
|
||||
|
||||
@ -166,10 +180,15 @@ class PaymentReconciliation(Document):
|
||||
def get_invoice_entries(self):
|
||||
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
|
||||
|
||||
condition = self.get_conditions(get_invoices=True)
|
||||
self.build_qb_filter_conditions(get_invoices=True)
|
||||
|
||||
non_reconciled_invoices = get_outstanding_invoices(
|
||||
self.party_type, self.party, self.receivable_payable_account, condition=condition
|
||||
self.party_type,
|
||||
self.party,
|
||||
self.receivable_payable_account,
|
||||
common_filter=self.common_filter_conditions,
|
||||
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
|
||||
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
|
||||
)
|
||||
|
||||
if self.invoice_limit:
|
||||
@ -329,89 +348,56 @@ class PaymentReconciliation(Document):
|
||||
if not invoices_to_reconcile:
|
||||
frappe.throw(_("No records found in Allocation table"))
|
||||
|
||||
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
|
||||
self.common_filter_conditions.clear()
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
self.common_filter_conditions.append(ple.company == self.company)
|
||||
|
||||
if self.get("cost_center") and (get_invoices or get_return_invoices):
|
||||
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
|
||||
|
||||
if get_invoices:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
|
||||
if self.from_invoice_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
|
||||
if self.to_invoice_date
|
||||
else ""
|
||||
)
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
|
||||
if self.minimum_invoice_amount:
|
||||
condition += " and {dr_or_cr} >= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
|
||||
)
|
||||
if self.maximum_invoice_amount:
|
||||
condition += " and {dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
|
||||
)
|
||||
if self.from_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
|
||||
if self.to_invoice_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
|
||||
|
||||
elif get_return_invoices:
|
||||
condition = " and doc.company = '{0}' ".format(self.company)
|
||||
condition += (
|
||||
" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
dr_or_cr = (
|
||||
"debit_in_account_currency"
|
||||
if erpnext.get_party_account_type(self.party_type) == "Receivable"
|
||||
else "credit_in_account_currency"
|
||||
)
|
||||
if self.from_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
|
||||
if self.to_payment_date:
|
||||
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
|
||||
|
||||
if self.minimum_invoice_amount:
|
||||
condition += " and gl.{dr_or_cr} >= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
|
||||
)
|
||||
if self.maximum_invoice_amount:
|
||||
condition += " and gl.{dr_or_cr} <= {amount}".format(
|
||||
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
|
||||
)
|
||||
def get_conditions(self, get_payments=False):
|
||||
condition = " and company = '{0}' ".format(self.company)
|
||||
|
||||
else:
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
if self.get("cost_center") and get_payments:
|
||||
condition = " and cost_center = '{0}' ".format(self.cost_center)
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
condition += (
|
||||
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
|
||||
if self.from_payment_date
|
||||
else ""
|
||||
)
|
||||
condition += (
|
||||
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
|
||||
if self.to_payment_date
|
||||
else ""
|
||||
)
|
||||
|
||||
if self.minimum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
|
||||
)
|
||||
if self.maximum_payment_amount:
|
||||
condition += (
|
||||
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
|
||||
if get_payments
|
||||
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
|
||||
)
|
||||
|
||||
return condition
|
||||
|
||||
|
@ -4,93 +4,542 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, getdate
|
||||
from frappe import qb
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, nowdate
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
|
||||
|
||||
class TestPaymentReconciliation(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
make_customer()
|
||||
make_invoice_and_payment()
|
||||
class TestPaymentReconciliation(FrappeTestCase):
|
||||
def setUp(self):
|
||||
self.create_company()
|
||||
self.create_item()
|
||||
self.create_customer()
|
||||
self.create_account()
|
||||
self.clear_old_entries()
|
||||
|
||||
def test_payment_reconciliation(self):
|
||||
payment_reco = frappe.get_doc("Payment Reconciliation")
|
||||
payment_reco.company = "_Test Company"
|
||||
payment_reco.party_type = "Customer"
|
||||
payment_reco.party = "_Test Payment Reco Customer"
|
||||
payment_reco.receivable_payable_account = "Debtors - _TC"
|
||||
payment_reco.from_invoice_date = add_days(getdate(), -1)
|
||||
payment_reco.to_invoice_date = getdate()
|
||||
payment_reco.from_payment_date = add_days(getdate(), -1)
|
||||
payment_reco.to_payment_date = getdate()
|
||||
payment_reco.maximum_invoice_amount = 1000
|
||||
payment_reco.maximum_payment_amount = 1000
|
||||
payment_reco.invoice_limit = 10
|
||||
payment_reco.payment_limit = 10
|
||||
payment_reco.bank_cash_account = "_Test Bank - _TC"
|
||||
payment_reco.cost_center = "_Test Cost Center - _TC"
|
||||
payment_reco.get_unreconciled_entries()
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
self.assertEqual(len(payment_reco.get("invoices")), 1)
|
||||
self.assertEqual(len(payment_reco.get("payments")), 1)
|
||||
def create_company(self):
|
||||
company = None
|
||||
if frappe.db.exists("Company", "_Test Payment Reconciliation"):
|
||||
company = frappe.get_doc("Company", "_Test Payment Reconciliation")
|
||||
else:
|
||||
company = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Company",
|
||||
"company_name": "_Test Payment Reconciliation",
|
||||
"country": "India",
|
||||
"default_currency": "INR",
|
||||
"create_chart_of_accounts_based_on": "Standard Template",
|
||||
"chart_of_accounts": "Standard",
|
||||
}
|
||||
)
|
||||
company = company.save()
|
||||
|
||||
payment_entry = payment_reco.get("payments")[0].reference_name
|
||||
invoice = payment_reco.get("invoices")[0].invoice_number
|
||||
self.company = company.name
|
||||
self.cost_center = company.cost_center
|
||||
self.warehouse = "All Warehouses - _PR"
|
||||
self.income_account = "Sales - _PR"
|
||||
self.expense_account = "Cost of Goods Sold - _PR"
|
||||
self.debit_to = "Debtors - _PR"
|
||||
self.creditors = "Creditors - _PR"
|
||||
|
||||
payment_reco.allocate_entries(
|
||||
{
|
||||
"payments": [payment_reco.get("payments")[0].as_dict()],
|
||||
"invoices": [payment_reco.get("invoices")[0].as_dict()],
|
||||
}
|
||||
# create bank account
|
||||
if frappe.db.exists("Account", "HDFC - _PR"):
|
||||
self.bank = "HDFC - _PR"
|
||||
else:
|
||||
bank_acc = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Account",
|
||||
"account_name": "HDFC",
|
||||
"parent_account": "Bank Accounts - _PR",
|
||||
"company": self.company,
|
||||
}
|
||||
)
|
||||
bank_acc.save()
|
||||
self.bank = bank_acc.name
|
||||
|
||||
def create_item(self):
|
||||
item = create_item(
|
||||
item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
|
||||
)
|
||||
payment_reco.reconcile()
|
||||
self.item = item if isinstance(item, str) else item.item_code
|
||||
|
||||
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
|
||||
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
|
||||
def create_customer(self):
|
||||
if frappe.db.exists("Customer", "_Test PR Customer"):
|
||||
self.customer = "_Test PR Customer"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer"
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer = customer.name
|
||||
|
||||
if frappe.db.exists("Customer", "_Test PR Customer 2"):
|
||||
self.customer2 = "_Test PR Customer 2"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer 2"
|
||||
customer.type = "Individual"
|
||||
customer.save()
|
||||
self.customer2 = customer.name
|
||||
|
||||
def make_customer():
|
||||
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Customer",
|
||||
"customer_name": "_Test Payment Reco Customer",
|
||||
"customer_type": "Individual",
|
||||
"customer_group": "_Test Customer Group",
|
||||
"territory": "_Test Territory",
|
||||
}
|
||||
).insert()
|
||||
if frappe.db.exists("Customer", "_Test PR Customer 3"):
|
||||
self.customer3 = "_Test PR Customer 3"
|
||||
else:
|
||||
customer = frappe.new_doc("Customer")
|
||||
customer.customer_name = "_Test PR Customer 3"
|
||||
customer.type = "Individual"
|
||||
customer.default_currency = "EUR"
|
||||
customer.save()
|
||||
self.customer3 = customer.name
|
||||
|
||||
def create_account(self):
|
||||
account_name = "Debtors EUR"
|
||||
if not frappe.db.get_value(
|
||||
"Account", filters={"account_name": account_name, "company": self.company}
|
||||
):
|
||||
acc = frappe.new_doc("Account")
|
||||
acc.account_name = account_name
|
||||
acc.parent_account = "Accounts Receivable - _PR"
|
||||
acc.company = self.company
|
||||
acc.account_currency = "EUR"
|
||||
acc.account_type = "Receivable"
|
||||
acc.insert()
|
||||
else:
|
||||
name = frappe.db.get_value(
|
||||
"Account",
|
||||
filters={"account_name": account_name, "company": self.company},
|
||||
fieldname="name",
|
||||
pluck=True,
|
||||
)
|
||||
acc = frappe.get_doc("Account", name)
|
||||
self.debtors_eur = acc.name
|
||||
|
||||
def make_invoice_and_payment():
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
|
||||
)
|
||||
si.cost_center = "_Test Cost Center - _TC"
|
||||
si.save()
|
||||
si.submit()
|
||||
def create_sales_invoice(
|
||||
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
|
||||
):
|
||||
"""
|
||||
Helper function to populate default values in sales invoice
|
||||
"""
|
||||
sinv = create_sales_invoice(
|
||||
qty=qty,
|
||||
rate=rate,
|
||||
company=self.company,
|
||||
customer=self.customer,
|
||||
item_code=self.item,
|
||||
item_name=self.item,
|
||||
cost_center=self.cost_center,
|
||||
warehouse=self.warehouse,
|
||||
debit_to=self.debit_to,
|
||||
parent_cost_center=self.cost_center,
|
||||
update_stock=0,
|
||||
currency="INR",
|
||||
is_pos=0,
|
||||
is_return=0,
|
||||
return_against=None,
|
||||
income_account=self.income_account,
|
||||
expense_account=self.expense_account,
|
||||
do_not_save=do_not_save,
|
||||
do_not_submit=do_not_submit,
|
||||
)
|
||||
return sinv
|
||||
|
||||
pe = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Entry",
|
||||
"payment_type": "Receive",
|
||||
"party_type": "Customer",
|
||||
"party": "_Test Payment Reco Customer",
|
||||
"company": "_Test Company",
|
||||
"paid_from_account_currency": "INR",
|
||||
"paid_to_account_currency": "INR",
|
||||
"source_exchange_rate": 1,
|
||||
"target_exchange_rate": 1,
|
||||
"reference_no": "1",
|
||||
"reference_date": getdate(),
|
||||
"received_amount": 690,
|
||||
"paid_amount": 690,
|
||||
"paid_from": "Debtors - _TC",
|
||||
"paid_to": "_Test Bank - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
}
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
def create_payment_entry(self, amount=100, posting_date=nowdate(), customer=None):
|
||||
"""
|
||||
Helper function to populate default values in payment entry
|
||||
"""
|
||||
payment = create_payment_entry(
|
||||
company=self.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=customer or self.customer,
|
||||
paid_from=self.debit_to,
|
||||
paid_to=self.bank,
|
||||
paid_amount=amount,
|
||||
)
|
||||
payment.posting_date = posting_date
|
||||
return payment
|
||||
|
||||
def clear_old_entries(self):
|
||||
doctype_list = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
"Journal Entry",
|
||||
]
|
||||
for doctype in doctype_list:
|
||||
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
|
||||
|
||||
def create_payment_reconciliation(self):
|
||||
pr = frappe.new_doc("Payment Reconciliation")
|
||||
pr.company = self.company
|
||||
pr.party_type = "Customer"
|
||||
pr.party = self.customer
|
||||
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
|
||||
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
|
||||
return pr
|
||||
|
||||
def create_journal_entry(
|
||||
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
|
||||
):
|
||||
je = frappe.new_doc("Journal Entry")
|
||||
je.posting_date = posting_date or nowdate()
|
||||
je.company = self.company
|
||||
je.user_remark = "test"
|
||||
if not cost_center:
|
||||
cost_center = self.cost_center
|
||||
je.set(
|
||||
"accounts",
|
||||
[
|
||||
{
|
||||
"account": acc1,
|
||||
"cost_center": cost_center,
|
||||
"debit_in_account_currency": amount if amount > 0 else 0,
|
||||
"credit_in_account_currency": abs(amount) if amount < 0 else 0,
|
||||
},
|
||||
{
|
||||
"account": acc2,
|
||||
"cost_center": cost_center,
|
||||
"credit_in_account_currency": amount if amount > 0 else 0,
|
||||
"debit_in_account_currency": abs(amount) if amount < 0 else 0,
|
||||
},
|
||||
],
|
||||
)
|
||||
return je
|
||||
|
||||
def test_filter_min_max(self):
|
||||
# check filter condition minimum and maximum amount
|
||||
self.create_sales_invoice(qty=1, rate=300)
|
||||
self.create_sales_invoice(qty=1, rate=400)
|
||||
self.create_sales_invoice(qty=1, rate=500)
|
||||
self.create_payment_entry(amount=300).save().submit()
|
||||
self.create_payment_entry(amount=400).save().submit()
|
||||
self.create_payment_entry(amount=500).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.minimum_invoice_amount = 400
|
||||
pr.maximum_invoice_amount = 500
|
||||
pr.minimum_payment_amount = 300
|
||||
pr.maximum_payment_amount = 600
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 3)
|
||||
|
||||
pr.minimum_invoice_amount = 300
|
||||
pr.maximum_invoice_amount = 600
|
||||
pr.minimum_payment_amount = 400
|
||||
pr.maximum_payment_amount = 500
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 3)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
|
||||
pr.minimum_invoice_amount = (
|
||||
pr.maximum_invoice_amount
|
||||
) = pr.minimum_payment_amount = pr.maximum_payment_amount = 0
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.get("invoices")), 3)
|
||||
self.assertEqual(len(pr.get("payments")), 3)
|
||||
|
||||
def test_filter_posting_date(self):
|
||||
# check filter condition using transaction date
|
||||
date1 = nowdate()
|
||||
date2 = add_days(nowdate(), -1)
|
||||
amount = 100
|
||||
self.create_sales_invoice(qty=1, rate=amount, posting_date=date1)
|
||||
si2 = self.create_sales_invoice(
|
||||
qty=1, rate=amount, posting_date=date2, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si2.set_posting_time = 1
|
||||
si2.posting_date = date2
|
||||
si2.save().submit()
|
||||
self.create_payment_entry(amount=amount, posting_date=date1).save().submit()
|
||||
self.create_payment_entry(amount=amount, posting_date=date2).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = pr.to_invoice_date = date1
|
||||
pr.from_payment_date = pr.to_payment_date = date1
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# assert only si and pe are fetched
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
|
||||
pr.from_invoice_date = date2
|
||||
pr.to_invoice_date = date1
|
||||
pr.from_payment_date = date2
|
||||
pr.to_payment_date = date1
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# assert only si and pe are fetched
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 2)
|
||||
|
||||
def test_filter_invoice_limit(self):
|
||||
# check filter condition - invoice limit
|
||||
transaction_date = nowdate()
|
||||
rate = 100
|
||||
invoices = []
|
||||
payments = []
|
||||
for i in range(5):
|
||||
invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
|
||||
pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
|
||||
payments.append(pe)
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.from_invoice_date = pr.to_invoice_date = transaction_date
|
||||
pr.from_payment_date = pr.to_payment_date = transaction_date
|
||||
pr.invoice_limit = 2
|
||||
pr.payment_limit = 3
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.get("invoices")), 2)
|
||||
self.assertEqual(len(pr.get("payments")), 3)
|
||||
|
||||
def test_payment_against_invoice(self):
|
||||
si = self.create_sales_invoice(qty=1, rate=200)
|
||||
pe = self.create_payment_entry(amount=55).save().submit()
|
||||
# second payment entry
|
||||
self.create_payment_entry(amount=35).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
# reconcile multiple payments against invoice
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Partly Paid")
|
||||
# check PR tool output post reconciliation
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 110)
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
# cancel one PE
|
||||
pe.reload()
|
||||
pe.cancel()
|
||||
pr.get_unreconciled_entries()
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 165)
|
||||
|
||||
def test_payment_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
|
||||
sales = "Sales - _PR"
|
||||
amount = 921
|
||||
# debit debtors account to record an invoice
|
||||
je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
|
||||
je.accounts[0].party_type = "Customer"
|
||||
je.accounts[0].party = self.customer
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
self.create_payment_entry(amount=amount, posting_date=transaction_date).save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
|
||||
pr.from_invoice_date = pr.to_invoice_date = transaction_date
|
||||
pr.from_payment_date = pr.to_payment_date = transaction_date
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
# credit debtors account to record a payment
|
||||
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je.accounts[1].party_type = "Customer"
|
||||
je.accounts[1].party = self.customer
|
||||
je.save()
|
||||
je.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
# check PR tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 0)
|
||||
self.assertEqual(len(pr.get("payments")), 0)
|
||||
|
||||
def test_journal_against_journal(self):
|
||||
transaction_date = nowdate()
|
||||
sales = "Sales - _PR"
|
||||
amount = 100
|
||||
|
||||
# debit debtors account to simulate a invoice
|
||||
je1 = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
|
||||
je1.accounts[0].party_type = "Customer"
|
||||
je1.accounts[0].party = self.customer
|
||||
je1.save()
|
||||
je1.submit()
|
||||
|
||||
# credit debtors account to simulate a payment
|
||||
je2 = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
|
||||
je2.accounts[1].party_type = "Customer"
|
||||
je2.accounts[1].party = self.customer
|
||||
je2.save()
|
||||
je2.submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
def test_cr_note_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.reconcile()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# check reconciliation tool output
|
||||
# reconciled invoice and credit note shouldn't show up in selection
|
||||
self.assertEqual(pr.get("invoices"), [])
|
||||
self.assertEqual(pr.get("payments"), [])
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_cr_note_partial_against_invoice(self):
|
||||
transaction_date = nowdate()
|
||||
amount = 100
|
||||
allocated_amount = 80
|
||||
|
||||
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.is_return = 1
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
invoices = [x.as_dict() for x in pr.get("invoices")]
|
||||
payments = [x.as_dict() for x in pr.get("payments")]
|
||||
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
|
||||
pr.allocation[0].allocated_amount = allocated_amount
|
||||
pr.reconcile()
|
||||
|
||||
# assert outstanding
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Partly Paid")
|
||||
self.assertEqual(si.outstanding_amount, 20)
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
# check reconciliation tool output
|
||||
self.assertEqual(len(pr.get("invoices")), 1)
|
||||
self.assertEqual(len(pr.get("payments")), 1)
|
||||
self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
|
||||
self.assertEqual(pr.get("payments")[0].amount, 20)
|
||||
|
||||
def test_pr_output_foreign_currency_and_amount(self):
|
||||
# test for currency and amount invoices and payments
|
||||
transaction_date = nowdate()
|
||||
# In EUR
|
||||
amount = 100
|
||||
exchange_rate = 80
|
||||
|
||||
si = self.create_sales_invoice(
|
||||
qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
si.customer = self.customer3
|
||||
si.currency = "EUR"
|
||||
si.conversion_rate = exchange_rate
|
||||
si.debit_to = self.debtors_eur
|
||||
si = si.save().submit()
|
||||
|
||||
cr_note = self.create_sales_invoice(
|
||||
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
|
||||
)
|
||||
cr_note.customer = self.customer3
|
||||
cr_note.is_return = 1
|
||||
cr_note.currency = "EUR"
|
||||
cr_note.conversion_rate = exchange_rate
|
||||
cr_note.debit_to = self.debtors_eur
|
||||
cr_note = cr_note.save().submit()
|
||||
|
||||
pr = self.create_payment_reconciliation()
|
||||
pr.party = self.customer3
|
||||
pr.receivable_payable_account = self.debtors_eur
|
||||
pr.get_unreconciled_entries()
|
||||
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
|
||||
self.assertEqual(pr.invoices[0].amount, amount)
|
||||
self.assertEqual(pr.invoices[0].currency, "EUR")
|
||||
self.assertEqual(pr.payments[0].amount, amount)
|
||||
self.assertEqual(pr.payments[0].currency, "EUR")
|
||||
|
||||
cr_note.cancel()
|
||||
|
||||
pay = self.create_payment_entry(
|
||||
amount=amount, posting_date=transaction_date, customer=self.customer3
|
||||
)
|
||||
pay.paid_from = self.debtors_eur
|
||||
pay.paid_from_account_currency = "EUR"
|
||||
pay.source_exchange_rate = exchange_rate
|
||||
pay.received_amount = exchange_rate * amount
|
||||
pay = pay.save().submit()
|
||||
|
||||
pr.get_unreconciled_entries()
|
||||
self.assertEqual(len(pr.invoices), 1)
|
||||
self.assertEqual(len(pr.payments), 1)
|
||||
self.assertEqual(pr.payments[0].amount, amount)
|
||||
self.assertEqual(pr.payments[0].currency, "EUR")
|
||||
|
@ -164,8 +164,6 @@
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
"is_opening",
|
||||
"c_form_applicable",
|
||||
"c_form_no",
|
||||
"column_break8",
|
||||
"remarks",
|
||||
"sales_team_section_break",
|
||||
@ -1399,23 +1397,6 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "c_form_applicable",
|
||||
"fieldtype": "Select",
|
||||
"label": "C-Form Applicable",
|
||||
"no_copy": 1,
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "c_form_no",
|
||||
"fieldtype": "Link",
|
||||
"label": "C-Form No",
|
||||
"no_copy": 1,
|
||||
"options": "C-Form",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break8",
|
||||
"fieldtype": "Column Break",
|
||||
|
@ -36,8 +36,12 @@ class PricingRule(Document):
|
||||
|
||||
def validate_duplicate_apply_on(self):
|
||||
if self.apply_on != "Transaction":
|
||||
field = apply_on_dict.get(self.apply_on)
|
||||
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
|
||||
apply_on_table = apply_on_dict.get(self.apply_on)
|
||||
if not apply_on_table:
|
||||
return
|
||||
|
||||
apply_on_field = frappe.scrub(self.apply_on)
|
||||
values = [d.get(apply_on_field) for d in self.get(apply_on_table) if d.get(apply_on_field)]
|
||||
if len(values) != len(set(values)):
|
||||
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))
|
||||
|
||||
|
@ -539,7 +539,7 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
},
|
||||
|
||||
add_custom_buttons: function(frm) {
|
||||
if (frm.doc.per_received < 100) {
|
||||
if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) {
|
||||
frm.add_custom_button(__('Purchase Receipt'), () => {
|
||||
frm.events.make_purchase_receipt(frm);
|
||||
}, __('Create'));
|
||||
|
@ -165,17 +165,6 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
super(PurchaseInvoice, self).set_missing_values(for_validate)
|
||||
|
||||
def check_conversion_rate(self):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
def validate_credit_to_acc(self):
|
||||
if not self.credit_to:
|
||||
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
|
||||
|
@ -1,3 +0,0 @@
|
||||
{% include "erpnext/regional/india/taxes.js" %}
|
||||
|
||||
erpnext.setup_auto_gst_taxation('Purchase Invoice');
|
@ -1474,15 +1474,30 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
def test_purchase_invoice_advance_taxes(self):
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
company = "_Test Company"
|
||||
|
||||
tds_account_args = {
|
||||
"doctype": "Account",
|
||||
"account_name": "TDS Payable",
|
||||
"account_type": "Tax",
|
||||
"parent_account": frappe.db.get_value(
|
||||
"Account", {"account_name": "Duties and Taxes", "company": company}
|
||||
),
|
||||
"company": company,
|
||||
}
|
||||
|
||||
tds_account = create_account(**tds_account_args)
|
||||
tax_withholding_category = "Test TDS - 194 - Dividends - Individual"
|
||||
|
||||
# Update tax withholding category with current fiscal year and rate details
|
||||
create_tax_witholding_category(tax_withholding_category, company, tds_account)
|
||||
|
||||
# create a new supplier to test
|
||||
supplier = create_supplier(
|
||||
supplier_name="_Test TDS Advance Supplier",
|
||||
tax_withholding_category="TDS - 194 - Dividends - Individual",
|
||||
tax_withholding_category=tax_withholding_category,
|
||||
)
|
||||
|
||||
# Update tax withholding category with current fiscal year and rate details
|
||||
update_tax_witholding_category("_Test Company", "TDS Payable - _TC")
|
||||
|
||||
# Create Purchase Order with TDS applied
|
||||
po = create_purchase_order(
|
||||
do_not_save=1,
|
||||
@ -1498,7 +1513,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name)
|
||||
payment_entry.paid_from = "Cash - _TC"
|
||||
payment_entry.apply_tax_withholding_amount = 1
|
||||
payment_entry.tax_withholding_category = "TDS - 194 - Dividends - Individual"
|
||||
payment_entry.tax_withholding_category = tax_withholding_category
|
||||
payment_entry.save()
|
||||
payment_entry.submit()
|
||||
|
||||
@ -1506,7 +1521,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
expected_gle = [
|
||||
["Cash - _TC", 0, 27000],
|
||||
["Creditors - _TC", 30000, 0],
|
||||
["TDS Payable - _TC", 0, 3000],
|
||||
[tds_account, 0, 3000],
|
||||
]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@ -1532,7 +1547,7 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
purchase_invoice.submit()
|
||||
|
||||
# Check GLE for Purchase Invoice
|
||||
# Zero net effect on final TDS Payable on invoice
|
||||
# Zero net effect on final TDS payable on invoice
|
||||
expected_gle = [["_Test Account Cost for Goods Sold - _TC", 30000], ["Creditors - _TC", -30000]]
|
||||
|
||||
gl_entries = frappe.db.sql(
|
||||
@ -1616,6 +1631,26 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
|
||||
def test_item_less_defaults(self):
|
||||
|
||||
pi = frappe.new_doc("Purchase Invoice")
|
||||
pi.supplier = "_Test Supplier"
|
||||
pi.company = "_Test Company"
|
||||
pi.append(
|
||||
"items",
|
||||
{
|
||||
"item_name": "Opening item",
|
||||
"qty": 1,
|
||||
"uom": "Tonne",
|
||||
"stock_uom": "Kg",
|
||||
"rate": 1000,
|
||||
"expense_account": "Stock Received But Not Billed - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
pi.save()
|
||||
self.assertEqual(pi.items[0].conversion_factor, 1000)
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
@ -1634,40 +1669,28 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
|
||||
def update_tax_witholding_category(company, account):
|
||||
def create_tax_witholding_category(category_name, company, account):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
fiscal_year = get_fiscal_year(date=nowdate())
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Tax Withholding Rate",
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"parent": "TDS - 194 - Dividends - Individual",
|
||||
"from_date": (">=", fiscal_year[1]),
|
||||
"to_date": ("<=", fiscal_year[2]),
|
||||
},
|
||||
):
|
||||
tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual")
|
||||
tds_category.set("rates", [])
|
||||
|
||||
tds_category.append(
|
||||
"rates",
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 2500,
|
||||
"cumulative_threshold": 0,
|
||||
},
|
||||
)
|
||||
tds_category.save()
|
||||
|
||||
if not frappe.db.get_value(
|
||||
"Tax Withholding Account", {"parent": "TDS - 194 - Dividends - Individual", "account": account}
|
||||
):
|
||||
tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual")
|
||||
tds_category.append("accounts", {"company": company, "account": account})
|
||||
tds_category.save()
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": category_name,
|
||||
"category_name": category_name,
|
||||
"accounts": [{"company": company, "account": account}],
|
||||
"rates": [
|
||||
{
|
||||
"from_date": fiscal_year[1],
|
||||
"to_date": fiscal_year[2],
|
||||
"tax_withholding_rate": 10,
|
||||
"single_threshold": 2500,
|
||||
"cumulative_threshold": 0,
|
||||
}
|
||||
],
|
||||
}
|
||||
).insert(ignore_if_duplicate=True)
|
||||
|
||||
|
||||
def unlink_payment_on_cancel_of_invoice(enable=1):
|
||||
|
@ -195,6 +195,7 @@
|
||||
"label": "Rejected Qty"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@ -214,6 +215,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "UOM Conversion Factor",
|
||||
@ -222,6 +224,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Accepted Qty in Stock UOM",
|
||||
@ -871,7 +874,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-15 17:04:07.191013",
|
||||
"modified": "2022-06-17 05:31:10.520171",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Item",
|
||||
@ -879,5 +882,6 @@
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
{% include "erpnext/regional/india/taxes.js" %}
|
||||
{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
|
||||
|
||||
erpnext.setup_auto_gst_taxation('Sales Invoice');
|
||||
erpnext.setup_einvoice_actions('Sales Invoice')
|
||||
|
||||
frappe.ui.form.on("Sales Invoice", {
|
||||
setup: function(frm) {
|
||||
frm.set_query('transporter', function() {
|
||||
return {
|
||||
filters: {
|
||||
'is_transporter': 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query('driver', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
'transporter': doc.transporter
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.docstatus == 1 && !frm.is_dirty()
|
||||
&& !frm.doc.is_return && !frm.doc.ewaybill) {
|
||||
|
||||
frm.add_custom_button('E-Way Bill JSON', () => {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.utils.generate_ewb_json',
|
||||
args: {
|
||||
'dt': frm.doc.doctype,
|
||||
'dn': [frm.doc.name]
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
const args = {
|
||||
cmd: 'erpnext.regional.india.utils.download_ewb_json',
|
||||
data: r.message,
|
||||
docname: frm.doc.name
|
||||
};
|
||||
open_url_post(frappe.request.url, args);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
}, __("Create"));
|
||||
}
|
||||
}
|
||||
|
||||
});
|
@ -1,174 +0,0 @@
|
||||
var globalOnload = frappe.listview_settings['Sales Invoice'].onload;
|
||||
frappe.listview_settings['Sales Invoice'].onload = function (list_view) {
|
||||
|
||||
// Provision in case onload event is added to sales_invoice.js in future
|
||||
if (globalOnload) {
|
||||
globalOnload(list_view);
|
||||
}
|
||||
|
||||
const action = () => {
|
||||
const selected_docs = list_view.get_checked_items();
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
|
||||
for (let doc of selected_docs) {
|
||||
if (doc.docstatus !== 1) {
|
||||
frappe.throw(__("E-Way Bill JSON can only be generated from a submitted document"));
|
||||
}
|
||||
}
|
||||
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.utils.generate_ewb_json',
|
||||
args: {
|
||||
'dt': list_view.doctype,
|
||||
'dn': docnames
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
const args = {
|
||||
cmd: 'erpnext.regional.india.utils.download_ewb_json',
|
||||
data: r.message,
|
||||
docname: docnames
|
||||
};
|
||||
open_url_post(frappe.request.url, args);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false);
|
||||
|
||||
const generate_irns = () => {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
if (docnames && docnames.length) {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices',
|
||||
args: { docnames },
|
||||
freeze: true,
|
||||
freeze_message: __('Generating E-Invoices...')
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint({
|
||||
message: __('Please select at least one sales invoice to generate IRN'),
|
||||
title: __('No Invoice Selected'),
|
||||
indicator: 'red'
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const cancel_irns = () => {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
|
||||
const fields = [
|
||||
{
|
||||
"label": "Reason",
|
||||
"fieldname": "reason",
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"default": "1-Duplicate",
|
||||
"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
|
||||
},
|
||||
{
|
||||
"label": "Remark",
|
||||
"fieldname": "remark",
|
||||
"fieldtype": "Data",
|
||||
"reqd": 1
|
||||
}
|
||||
];
|
||||
|
||||
const d = new frappe.ui.Dialog({
|
||||
title: __("Cancel IRN"),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
const data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_irns',
|
||||
args: {
|
||||
doctype: list_view.doctype,
|
||||
docnames,
|
||||
reason: data.reason.split('-')[0],
|
||||
remark: data.remark
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('Cancelling E-Invoices...'),
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __('Submit')
|
||||
});
|
||||
d.show();
|
||||
};
|
||||
|
||||
let einvoicing_enabled = false;
|
||||
frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => {
|
||||
einvoicing_enabled = enabled;
|
||||
});
|
||||
|
||||
list_view.$result.on("change", "input[type=checkbox]", () => {
|
||||
if (einvoicing_enabled) {
|
||||
const docnames = list_view.get_checked_items(true);
|
||||
// show/hide e-invoicing actions when no sales invoices are checked
|
||||
if (docnames && docnames.length) {
|
||||
// prevent adding actions twice if e-invoicing action group already exists
|
||||
if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) {
|
||||
list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing'));
|
||||
list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing'));
|
||||
}
|
||||
} else {
|
||||
list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing'));
|
||||
list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("bulk_einvoice_generation_complete", (data) => {
|
||||
const { failures, user, invoices } = data;
|
||||
|
||||
if (invoices.length != failures.length) {
|
||||
frappe.msgprint({
|
||||
message: __('{0} e-invoices generated successfully', [invoices.length]),
|
||||
title: __('Bulk E-Invoice Generation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
if (failures && failures.length && user == frappe.session.user) {
|
||||
let message = `
|
||||
Failed to generate IRNs for following ${failures.length} sales invoices:
|
||||
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
frappe.msgprint({
|
||||
message: message,
|
||||
title: __('Bulk E-Invoice Generation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => {
|
||||
const { failures, user, invoices } = data;
|
||||
|
||||
if (invoices.length != failures.length) {
|
||||
frappe.msgprint({
|
||||
message: __('{0} e-invoices cancelled successfully', [invoices.length]),
|
||||
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
|
||||
if (failures && failures.length && user == frappe.session.user) {
|
||||
let message = `
|
||||
Failed to cancel IRNs for following ${failures.length} sales invoices:
|
||||
<ul style="padding-left: 20px; padding-top: 5px;">
|
||||
${failures.map(d => `<li>${d.docname}</li>`).join('')}
|
||||
</ul>
|
||||
`;
|
||||
frappe.msgprint({
|
||||
message: message,
|
||||
title: __('Bulk E-Invoice Cancellation Complete'),
|
||||
indicator: 'orange'
|
||||
});
|
||||
}
|
||||
});
|
||||
};
|
@ -476,6 +476,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
this.frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
}
|
||||
|
||||
is_cash_or_non_trade_discount() {
|
||||
this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount);
|
||||
if (!this.frm.doc.is_cash_or_non_trade_discount) {
|
||||
this.frm.set_value("additional_discount_account", "");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// for backward compatibility: combine new and previous states
|
||||
@ -783,10 +790,6 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
}
|
||||
|
||||
// India related fields
|
||||
if (frappe.boot.sysdefaults.country == 'India') unhide_field(['c_form_applicable', 'c_form_no']);
|
||||
else hide_field(['c_form_applicable', 'c_form_no']);
|
||||
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
|
@ -106,6 +106,7 @@
|
||||
"loyalty_redemption_cost_center",
|
||||
"section_break_49",
|
||||
"apply_discount_on",
|
||||
"is_cash_or_non_trade_discount",
|
||||
"base_discount_amount",
|
||||
"additional_discount_account",
|
||||
"column_break_51",
|
||||
@ -174,8 +175,6 @@
|
||||
"debit_to",
|
||||
"party_account_currency",
|
||||
"is_opening",
|
||||
"c_form_applicable",
|
||||
"c_form_no",
|
||||
"column_break8",
|
||||
"unrealized_profit_loss_account",
|
||||
"remarks",
|
||||
@ -1716,28 +1715,6 @@
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "c_form_applicable",
|
||||
"fieldtype": "Select",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "C-Form Applicable",
|
||||
"length": 4,
|
||||
"no_copy": 1,
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "c_form_no",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "C-Form No",
|
||||
"no_copy": 1,
|
||||
"options": "C-Form",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break8",
|
||||
"fieldtype": "Column Break",
|
||||
@ -1790,8 +1767,6 @@
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fetch_from": "sales_partner.commission_rate",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "commission_rate",
|
||||
"fieldtype": "Float",
|
||||
"hide_days": 1,
|
||||
@ -1990,7 +1965,7 @@
|
||||
{
|
||||
"fieldname": "additional_discount_account",
|
||||
"fieldtype": "Link",
|
||||
"label": "Additional Discount Account",
|
||||
"label": "Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
@ -2028,6 +2003,13 @@
|
||||
"fieldtype": "Currency",
|
||||
"label": "Amount Eligible for Commission",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval: doc.apply_discount_on == \"Grand Total\"",
|
||||
"fieldname": "is_cash_or_non_trade_discount",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Cash or Non Trade Discount"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@ -2040,7 +2022,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-10 03:52:51.409913",
|
||||
"modified": "2022-06-16 16:22:44.870575",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -114,6 +114,7 @@ class SalesInvoice(SellingController):
|
||||
self.set_income_account_for_fixed_assets()
|
||||
self.validate_item_cost_centers()
|
||||
self.validate_income_account()
|
||||
self.check_conversion_rate()
|
||||
|
||||
validate_inter_company_party(
|
||||
self.doctype, self.customer, self.company, self.inter_company_invoice_reference
|
||||
@ -150,7 +151,6 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
self.set_against_income_account()
|
||||
self.validate_c_form()
|
||||
self.validate_time_sheets_are_submitted()
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
||||
if not self.is_return:
|
||||
@ -365,8 +365,6 @@ class SalesInvoice(SellingController):
|
||||
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
|
||||
self.update_serial_no(in_cancel=True)
|
||||
|
||||
self.validate_c_form_on_cancel()
|
||||
|
||||
# Updating stock ledger should always be called after updating prevdoc status,
|
||||
# because updating reserved qty in bin depends upon updated delivered qty in SO
|
||||
if self.update_stock == 1:
|
||||
@ -814,25 +812,6 @@ class SalesInvoice(SellingController):
|
||||
if flt(self.change_amount) and not self.account_for_change_amount:
|
||||
msgprint(_("Please enter Account for Change Amount"), raise_exception=1)
|
||||
|
||||
def validate_c_form(self):
|
||||
"""Blank C-form no if C-form applicable marked as 'No'"""
|
||||
if self.amended_from and self.c_form_applicable == "No" and self.c_form_no:
|
||||
frappe.db.sql(
|
||||
"""delete from `tabC-Form Invoice Detail` where invoice_no = %s
|
||||
and parent = %s""",
|
||||
(self.amended_from, self.c_form_no),
|
||||
)
|
||||
|
||||
frappe.db.set(self, "c_form_no", "")
|
||||
|
||||
def validate_c_form_on_cancel(self):
|
||||
"""Display message if C-Form no exists on cancellation of Sales Invoice"""
|
||||
if self.c_form_applicable == "Yes" and self.c_form_no:
|
||||
msgprint(
|
||||
_("Please remove this Invoice {0} from C-Form {1}").format(self.name, self.c_form_no),
|
||||
raise_exception=1,
|
||||
)
|
||||
|
||||
def validate_dropship_item(self):
|
||||
for item in self.items:
|
||||
if item.sales_order:
|
||||
@ -1029,7 +1008,7 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
|
||||
if grand_total and not self.is_internal_transfer():
|
||||
# Didnot use base_grand_total to book rounding loss gle
|
||||
# Did not use base_grand_total to book rounding loss gle
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
@ -1054,6 +1033,22 @@ class SalesInvoice(SellingController):
|
||||
)
|
||||
)
|
||||
|
||||
if self.apply_discount_on == "Grand Total" and self.get("is_cash_or_discount_account"):
|
||||
gl_entries.append(
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.additional_discount_account,
|
||||
"against": self.debit_to,
|
||||
"debit": self.base_discount_amount,
|
||||
"debit_in_account_currency": self.discount_amount,
|
||||
"cost_center": self.cost_center,
|
||||
"project": self.project,
|
||||
},
|
||||
self.currency,
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
def make_tax_gl_entries(self, gl_entries):
|
||||
enable_discount_accounting = cint(
|
||||
frappe.db.get_single_value("Selling Settings", "enable_discount_accounting")
|
||||
@ -1511,9 +1506,7 @@ class SalesInvoice(SellingController):
|
||||
frappe.get_doc("Delivery Note", dn).update_billing_percentage(update_modified=update_modified)
|
||||
|
||||
def on_recurring(self, reference_doc, auto_repeat_doc):
|
||||
for fieldname in ("c_form_applicable", "c_form_no", "write_off_amount"):
|
||||
self.set(fieldname, reference_doc.get(fieldname))
|
||||
|
||||
self.set("write_off_amount", reference_doc.get("write_off_amount"))
|
||||
self.due_date = None
|
||||
|
||||
def update_serial_no(self, in_cancel=False):
|
||||
@ -2116,6 +2109,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
source_document_warehouse_field = "from_warehouse"
|
||||
target_document_warehouse_field = "target_warehouse"
|
||||
|
||||
received_items = get_received_items(source_name, target_doctype, target_detail_field)
|
||||
|
||||
validate_inter_company_transaction(source_doc, doctype)
|
||||
details = get_inter_company_details(source_doc, doctype)
|
||||
|
||||
@ -2180,12 +2175,17 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
shipping_address_name=target_doc.shipping_address_name,
|
||||
)
|
||||
|
||||
def update_item(source, target, source_parent):
|
||||
target.qty = flt(source.qty) - received_items.get(source.name, 0.0)
|
||||
|
||||
item_field_map = {
|
||||
"doctype": target_doctype + " Item",
|
||||
"field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"],
|
||||
"field_map": {
|
||||
"rate": "rate",
|
||||
},
|
||||
"postprocess": update_item,
|
||||
"condition": lambda doc: doc.qty > 0,
|
||||
}
|
||||
|
||||
if doctype in ["Sales Invoice", "Sales Order"]:
|
||||
@ -2223,6 +2223,28 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
|
||||
return doclist
|
||||
|
||||
|
||||
def get_received_items(reference_name, doctype, reference_fieldname):
|
||||
target_doctypes = frappe.get_all(
|
||||
doctype,
|
||||
filters={"inter_company_invoice_reference": reference_name, "docstatus": 1},
|
||||
as_list=True,
|
||||
)
|
||||
|
||||
if target_doctypes:
|
||||
target_doctypes = list(target_doctypes[0])
|
||||
|
||||
received_items_map = frappe._dict(
|
||||
frappe.get_all(
|
||||
doctype + " Item",
|
||||
filters={"parent": ("in", target_doctypes)},
|
||||
fields=[reference_fieldname, "qty"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
return received_items_map
|
||||
|
||||
|
||||
def set_purchase_references(doc):
|
||||
# add internal PO or PR links if any
|
||||
if doc.is_internal_transfer():
|
||||
|
@ -11,6 +11,7 @@ def get_data():
|
||||
"Payment Request": "reference_name",
|
||||
"Sales Invoice": "return_against",
|
||||
"Auto Repeat": "reference_document",
|
||||
"Purchase Invoice": "inter_company_invoice_reference",
|
||||
},
|
||||
"internal_links": {
|
||||
"Sales Order": ["items", "sales_order"],
|
||||
@ -30,5 +31,6 @@ def get_data():
|
||||
{"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]},
|
||||
{"label": _("Returns"), "items": ["Sales Invoice"]},
|
||||
{"label": _("Subscription"), "items": ["Auto Repeat"]},
|
||||
{"label": _("Internal Transfers"), "items": ["Purchase Invoice"]},
|
||||
],
|
||||
}
|
||||
|
@ -24,7 +24,6 @@ from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_d
|
||||
from erpnext.controllers.accounts_controller import update_invoice_status
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
|
||||
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
|
||||
from erpnext.regional.india.utils import get_ewb_data
|
||||
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
|
||||
from erpnext.stock.doctype.item.test_item import create_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
@ -792,6 +791,54 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
jv.cancel()
|
||||
self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0)
|
||||
|
||||
def test_outstanding_on_cost_center_allocation(self):
|
||||
# setup cost centers
|
||||
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
|
||||
from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
|
||||
create_cost_center_allocation,
|
||||
)
|
||||
|
||||
cost_centers = [
|
||||
"Main Cost Center 1",
|
||||
"Sub Cost Center 1",
|
||||
"Sub Cost Center 2",
|
||||
]
|
||||
for cc in cost_centers:
|
||||
create_cost_center(cost_center_name=cc, company="_Test Company")
|
||||
|
||||
cca = create_cost_center_allocation(
|
||||
"_Test Company",
|
||||
"Main Cost Center 1 - _TC",
|
||||
{"Sub Cost Center 1 - _TC": 60, "Sub Cost Center 2 - _TC": 40},
|
||||
)
|
||||
|
||||
# make invoice
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.is_pos = 0
|
||||
si.insert()
|
||||
si.submit()
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
# make payment - fully paid
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_from_account_currency = si.currency
|
||||
pe.paid_to_account_currency = si.currency
|
||||
pe.source_exchange_rate = 1
|
||||
pe.target_exchange_rate = 1
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.cost_center = cca.main_cost_center
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
# cancel cost center allocation
|
||||
cca.cancel()
|
||||
|
||||
si.reload()
|
||||
self.assertEqual(si.outstanding_amount, 0)
|
||||
|
||||
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
|
||||
si = frappe.copy_doc(test_records[1])
|
||||
si.insert()
|
||||
@ -1583,6 +1630,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
self.assertTrue(gle)
|
||||
|
||||
def test_invoice_exchange_rate(self):
|
||||
si = create_sales_invoice(
|
||||
customer="_Test Customer USD",
|
||||
debit_to="_Test Receivable USD - _TC",
|
||||
currency="USD",
|
||||
conversion_rate=1,
|
||||
do_not_save=1,
|
||||
)
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.save)
|
||||
|
||||
def test_invalid_currency(self):
|
||||
# Customer currency = USD
|
||||
|
||||
@ -1786,24 +1844,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
for i, k in enumerate(expected_values["keys"]):
|
||||
self.assertEqual(d.get(k), expected_values[d.item_code][i])
|
||||
|
||||
def test_item_wise_tax_breakup_india(self):
|
||||
frappe.flags.country = "India"
|
||||
|
||||
si = self.create_si_to_test_tax_breakup()
|
||||
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
|
||||
|
||||
expected_itemised_tax = {
|
||||
"_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
|
||||
"_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
|
||||
}
|
||||
expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
|
||||
|
||||
self.assertEqual(itemised_tax, expected_itemised_tax)
|
||||
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
|
||||
|
||||
frappe.flags.country = None
|
||||
|
||||
def test_item_wise_tax_breakup_outside_india(self):
|
||||
def test_item_wise_tax_breakup(self):
|
||||
frappe.flags.country = "United States"
|
||||
|
||||
si = self.create_si_to_test_tax_breakup()
|
||||
@ -1827,7 +1868,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 100,
|
||||
"rate": 50,
|
||||
@ -1840,7 +1880,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item 2",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 100,
|
||||
"rate": 50,
|
||||
@ -1927,7 +1966,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"items",
|
||||
{
|
||||
"item_code": "_Test Item",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
"qty": 1,
|
||||
"rate": rate,
|
||||
@ -2600,78 +2638,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
|
||||
|
||||
def test_eway_bill_json(self):
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
|
||||
si.submit()
|
||||
|
||||
data = get_ewb_data("Sales Invoice", [si.name])
|
||||
|
||||
self.assertEqual(data["version"], "1.0.0421")
|
||||
self.assertEqual(data["billLists"][0]["fromGstin"], "27AAECE4835E1ZR")
|
||||
self.assertEqual(data["billLists"][0]["fromTrdName"], "_Test Company")
|
||||
self.assertEqual(data["billLists"][0]["toTrdName"], "_Test Customer")
|
||||
self.assertEqual(data["billLists"][0]["vehicleType"], "R")
|
||||
self.assertEqual(data["billLists"][0]["totalValue"], 60000)
|
||||
self.assertEqual(data["billLists"][0]["cgstValue"], 5400)
|
||||
self.assertEqual(data["billLists"][0]["sgstValue"], 5400)
|
||||
self.assertEqual(data["billLists"][0]["vehicleNo"], "KA12KA1234")
|
||||
self.assertEqual(data["billLists"][0]["itemList"][0]["taxableAmount"], 60000)
|
||||
self.assertEqual(data["billLists"][0]["actualFromStateCode"], 7)
|
||||
self.assertEqual(data["billLists"][0]["fromStateCode"], 27)
|
||||
|
||||
def test_einvoice_submission_without_irn(self):
|
||||
# init
|
||||
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"
|
||||
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
|
||||
si.irn = "test_irn"
|
||||
si.submit()
|
||||
|
||||
# reset
|
||||
einvoice_settings = frappe.get_doc("E Invoice Settings")
|
||||
einvoice_settings.enable = 0
|
||||
einvoice_settings.save()
|
||||
frappe.flags.country = country
|
||||
|
||||
def test_einvoice_json(self):
|
||||
from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
|
||||
|
||||
si = get_sales_invoice_for_e_invoice()
|
||||
si.discount_amount = 100
|
||||
si.save()
|
||||
|
||||
einvoice = make_einvoice(si)
|
||||
self.assertTrue(einvoice["EwbDtls"])
|
||||
validate_totals(einvoice)
|
||||
|
||||
si.apply_discount_on = "Net Total"
|
||||
si.save()
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
[d.set("included_in_print_rate", 1) for d in si.taxes]
|
||||
si.save()
|
||||
einvoice = make_einvoice(si)
|
||||
validate_totals(einvoice)
|
||||
|
||||
def test_item_tax_net_range(self):
|
||||
item = create_item("T Shirt")
|
||||
|
||||
@ -3142,7 +3108,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
def test_sales_invoice_with_disabled_account(self):
|
||||
try:
|
||||
account = frappe.get_doc("Account", "VAT 5% - _TC")
|
||||
account_name = "Sales Expenses - _TC"
|
||||
account = frappe.get_doc("Account", account_name)
|
||||
account.disabled = 1
|
||||
account.save()
|
||||
|
||||
@ -3154,10 +3121,10 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "VAT 5% - _TC",
|
||||
"account_head": account_name,
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "VAT @ 5.0",
|
||||
"rate": 9,
|
||||
"description": "Commission",
|
||||
"rate": 5,
|
||||
},
|
||||
)
|
||||
si.save()
|
||||
@ -3265,177 +3232,6 @@ def get_sales_invoice_for_e_invoice():
|
||||
return si
|
||||
|
||||
|
||||
def make_test_address_for_ewaybill():
|
||||
if not frappe.db.exists("Address", "_Test Address for Eway bill-Billing"):
|
||||
address = frappe.get_doc(
|
||||
{
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_line2": "_Test Address Line 2",
|
||||
"address_title": "_Test Address for Eway bill",
|
||||
"address_type": "Billing",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 1,
|
||||
"phone": "+910000000000",
|
||||
"gstin": "27AAECE4835E1ZR",
|
||||
"gst_state": "Maharashtra",
|
||||
"gst_state_number": "27",
|
||||
"pincode": "401108",
|
||||
}
|
||||
).insert()
|
||||
|
||||
address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"})
|
||||
|
||||
address.save()
|
||||
|
||||
if not frappe.db.exists("Address", "_Test Customer-Address for Eway bill-Billing"):
|
||||
address = frappe.get_doc(
|
||||
{
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_line2": "_Test Address Line 2",
|
||||
"address_title": "_Test Customer-Address for Eway bill",
|
||||
"address_type": "Billing",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 1,
|
||||
"phone": "+910000000000",
|
||||
"gstin": "27AACCM7806M1Z3",
|
||||
"gst_state": "Maharashtra",
|
||||
"gst_state_number": "27",
|
||||
"pincode": "410038",
|
||||
}
|
||||
).insert()
|
||||
|
||||
address.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
|
||||
|
||||
address.save()
|
||||
|
||||
if not frappe.db.exists("Address", "_Test Customer-Address for Eway bill-Shipping"):
|
||||
address = frappe.get_doc(
|
||||
{
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_line2": "_Test Address Line 2",
|
||||
"address_title": "_Test Customer-Address for Eway bill",
|
||||
"address_type": "Shipping",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 1,
|
||||
"phone": "+910000000000",
|
||||
"gst_state": "Maharashtra",
|
||||
"gst_state_number": "27",
|
||||
"pincode": "410098",
|
||||
}
|
||||
).insert()
|
||||
|
||||
address.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
|
||||
|
||||
address.save()
|
||||
|
||||
if not frappe.db.exists("Address", "_Test Dispatch-Address for Eway bill-Shipping"):
|
||||
address = frappe.get_doc(
|
||||
{
|
||||
"address_line1": "_Test Dispatch Address Line 1",
|
||||
"address_line2": "_Test Dispatch Address Line 2",
|
||||
"address_title": "_Test Dispatch-Address for Eway bill",
|
||||
"address_type": "Shipping",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 0,
|
||||
"phone": "+910000000000",
|
||||
"gstin": "07AAACC1206D1ZI",
|
||||
"gst_state": "Delhi",
|
||||
"gst_state_number": "07",
|
||||
"pincode": "1100101",
|
||||
}
|
||||
).insert()
|
||||
|
||||
address.save()
|
||||
|
||||
|
||||
def make_test_transporter_for_ewaybill():
|
||||
if not frappe.db.exists("Supplier", "_Test Transporter"):
|
||||
frappe.get_doc(
|
||||
{
|
||||
"doctype": "Supplier",
|
||||
"supplier_name": "_Test Transporter",
|
||||
"country": "India",
|
||||
"supplier_group": "_Test Supplier Group",
|
||||
"supplier_type": "Company",
|
||||
"is_transporter": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def make_sales_invoice_for_ewaybill():
|
||||
make_test_address_for_ewaybill()
|
||||
make_test_transporter_for_ewaybill()
|
||||
|
||||
gst_settings = frappe.get_doc("GST Settings")
|
||||
|
||||
gst_account = frappe.get_all(
|
||||
"GST Account",
|
||||
fields=["cgst_account", "sgst_account", "igst_account"],
|
||||
filters={"company": "_Test Company"},
|
||||
)
|
||||
|
||||
if not gst_account:
|
||||
gst_settings.append(
|
||||
"gst_accounts",
|
||||
{
|
||||
"company": "_Test Company",
|
||||
"cgst_account": "Output Tax CGST - _TC",
|
||||
"sgst_account": "Output Tax SGST - _TC",
|
||||
"igst_account": "Output Tax IGST - _TC",
|
||||
},
|
||||
)
|
||||
|
||||
gst_settings.save()
|
||||
|
||||
si = create_sales_invoice(do_not_save=1, rate="60000")
|
||||
|
||||
si.distance = 2000
|
||||
si.company_address = "_Test Address for Eway bill-Billing"
|
||||
si.customer_address = "_Test Customer-Address for Eway bill-Billing"
|
||||
si.shipping_address_name = "_Test Customer-Address for Eway bill-Shipping"
|
||||
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
|
||||
si.vehicle_no = "KA12KA1234"
|
||||
si.gst_category = "Registered Regular"
|
||||
si.mode_of_transport = "Road"
|
||||
si.transporter = "_Test Transporter"
|
||||
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "Output Tax CGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "CGST @ 9.0",
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
|
||||
si.append(
|
||||
"taxes",
|
||||
{
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": "Output Tax SGST - _TC",
|
||||
"cost_center": "Main - _TC",
|
||||
"description": "SGST @ 9.0",
|
||||
"rate": 9,
|
||||
},
|
||||
)
|
||||
|
||||
return si
|
||||
|
||||
|
||||
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
gl_entries = frappe.db.sql(
|
||||
"""select account, debit, credit, posting_date
|
||||
@ -3478,7 +3274,6 @@ def create_sales_invoice(**args):
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"item_name": args.item_name or "_Test Item",
|
||||
"description": args.description or "_Test Item",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 1,
|
||||
"uom": args.uom or "Nos",
|
||||
@ -3531,7 +3326,6 @@ def create_sales_invoice_against_cost_center(**args):
|
||||
"items",
|
||||
{
|
||||
"item_code": args.item or args.item_code or "_Test Item",
|
||||
"gst_hsn_code": "999800",
|
||||
"warehouse": args.warehouse or "_Test Warehouse - _TC",
|
||||
"qty": args.qty or 1,
|
||||
"rate": args.rate or 100,
|
||||
|
@ -182,6 +182,7 @@
|
||||
"oldfieldtype": "Currency"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@ -200,6 +201,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "UOM Conversion Factor",
|
||||
@ -207,6 +209,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty as per Stock UOM",
|
||||
@ -843,7 +846,7 @@
|
||||
"idx": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-03-23 08:18:04.928287",
|
||||
"modified": "2022-06-17 05:33:15.335912",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Item",
|
||||
|
@ -145,13 +145,14 @@ class Subscription(Document):
|
||||
You shouldn't need to call this directly. Use `get_billing_cycle` instead.
|
||||
"""
|
||||
plan_names = [plan.plan for plan in self.plans]
|
||||
billing_info = frappe.db.sql(
|
||||
"select distinct `billing_interval`, `billing_interval_count` "
|
||||
"from `tabSubscription Plan` "
|
||||
"where name in %s",
|
||||
(plan_names,),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
subscription_plan = frappe.qb.DocType("Subscription Plan")
|
||||
billing_info = (
|
||||
frappe.qb.from_(subscription_plan)
|
||||
.select(subscription_plan.billing_interval, subscription_plan.billing_interval_count)
|
||||
.distinct()
|
||||
.where(subscription_plan.name.isin(plan_names))
|
||||
).run(as_dict=1)
|
||||
|
||||
return billing_info
|
||||
|
||||
|
@ -35,7 +35,13 @@ def make_gl_entries(
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
create_payment_ledger_entry(gl_map)
|
||||
create_payment_ledger_entry(
|
||||
gl_map,
|
||||
cancel=0,
|
||||
adv_adj=adv_adj,
|
||||
update_outstanding=update_outstanding,
|
||||
from_repost=from_repost,
|
||||
)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
@ -126,7 +132,7 @@ def distribute_gl_based_on_cost_center_allocation(gl_map, precision=None):
|
||||
for sub_cost_center, percentage in cost_center_allocation.get(cost_center, {}).items():
|
||||
gle = copy.deepcopy(d)
|
||||
gle.cost_center = sub_cost_center
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_company_currency"):
|
||||
for field in ("debit", "credit", "debit_in_account_currency", "credit_in_account_currency"):
|
||||
gle[field] = flt(flt(d.get(field)) * percentage / 100, precision)
|
||||
new_gl_map.append(gle)
|
||||
else:
|
||||
@ -482,6 +488,9 @@ def make_reverse_gl_entries(
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||
create_payment_ledger_entry(
|
||||
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
|
||||
)
|
||||
validate_accounting_period(gl_entries)
|
||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||
|
@ -1,180 +0,0 @@
|
||||
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
|
||||
|
||||
<div class="page-break">
|
||||
{% if doc.signed_einvoice %}
|
||||
{%- set einvoice = json.loads(doc.signed_einvoice) -%}
|
||||
<div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
|
||||
{% if letter_head and not no_letterhead %}
|
||||
<div class="letter-head">{{ letter_head }}</div>
|
||||
{% endif %}
|
||||
<div class="print-heading">
|
||||
<h2>E Invoice<br><small>{{ doc.name }}</small></h2>
|
||||
</div>
|
||||
</div>
|
||||
{% if print_settings.repeat_header_footer %}
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if not no_letterhead and footer %}
|
||||
<div class="letter-head-footer">
|
||||
{{ footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
<p class="text-center small page-number visible-pdf">
|
||||
{{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
|
||||
</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
<h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
|
||||
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||
<div class="col-xs-8 column-break">
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>IRN</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.Irn }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Ack. No</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Ack. Date</label></div>
|
||||
<div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Category</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Document Type</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
|
||||
</div>
|
||||
<div class="row data-field">
|
||||
<div class="col-xs-4"><label>Document No</label></div>
|
||||
<div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-xs-4 column-break">
|
||||
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
|
||||
</div>
|
||||
</div>
|
||||
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
|
||||
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
|
||||
{%- set seller = einvoice.SellerDtls -%}
|
||||
<div class="col-xs-6 column-break">
|
||||
<h5 style="margin-bottom: 5px;">Seller</h5>
|
||||
<p>{{ seller.Gstin }}</p>
|
||||
<p>{{ seller.LglNm }}</p>
|
||||
<p>{{ seller.Addr1 }}</p>
|
||||
{%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
|
||||
<p>{{ seller.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
|
||||
|
||||
{%- if einvoice.ShipDtls -%}
|
||||
{%- set shipping = einvoice.ShipDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Shipped From</h5>
|
||||
<p>{{ shipping.Gstin }}</p>
|
||||
<p>{{ shipping.LglNm }}</p>
|
||||
<p>{{ shipping.Addr1 }}</p>
|
||||
{%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
|
||||
<p>{{ shipping.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{%- set buyer = einvoice.BuyerDtls -%}
|
||||
<div class="col-xs-6 column-break">
|
||||
<h5 style="margin-bottom: 5px;">Buyer</h5>
|
||||
<p>{{ buyer.Gstin }}</p>
|
||||
<p>{{ buyer.LglNm }}</p>
|
||||
<p>{{ buyer.Addr1 }}</p>
|
||||
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
|
||||
<p>{{ buyer.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
|
||||
|
||||
{%- if einvoice.DispDtls -%}
|
||||
{%- set dispatch = einvoice.DispDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
|
||||
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
|
||||
<p>{{ dispatch.LglNm }}</p>
|
||||
<p>{{ dispatch.Addr1 }}</p>
|
||||
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
|
||||
<p>{{ dispatch.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left" style="width: 3%;">Sr. No.</th>
|
||||
<th class="text-left">Item</th>
|
||||
<th class="text-left" style="width: 10%;">HSN Code</th>
|
||||
<th class="text-left" style="width: 5%;">Qty</th>
|
||||
<th class="text-left" style="width: 5%;">UOM</th>
|
||||
<th class="text-left">Rate</th>
|
||||
<th class="text-left" style="width: 5%;">Discount</th>
|
||||
<th class="text-left">Taxable Amount</th>
|
||||
<th class="text-left" style="width: 7%;">Tax Rate</th>
|
||||
<th class="text-left" style="width: 5%;">Other Charges</th>
|
||||
<th class="text-left">Total</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in einvoice.ItemList %}
|
||||
<tr>
|
||||
<td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
|
||||
<td class="text-left">{{ item.PrdDesc }}</td>
|
||||
<td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
|
||||
<td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
|
||||
<td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
|
||||
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
|
||||
<td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
|
||||
<td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
<h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="text-left">Taxable Amount</th>
|
||||
<th class="text-left">CGST</th>
|
||||
<th class="text-left"">SGST</th>
|
||||
<th class="text-left">IGST</th>
|
||||
<th class="text-left">CESS</th>
|
||||
<th class="text-left" style="width: 10%;">State CESS</th>
|
||||
<th class="text-left">Discount</th>
|
||||
<th class="text-left" style="width: 10%;">Other Charges</th>
|
||||
<th class="text-left" style="width: 10%;">Round Off</th>
|
||||
<th class="text-left">Total Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{%- set value_details = einvoice.ValDtls -%}
|
||||
<tr>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
|
||||
<td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center" style="color: var(--gray-500); font-size: 14px;">
|
||||
You must generate IRN before you can preview GST E-Invoice.
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
@ -1,24 +0,0 @@
|
||||
{
|
||||
"align_labels_right": 1,
|
||||
"creation": "2020-10-10 18:01:21.032914",
|
||||
"custom_format": 0,
|
||||
"default_print_language": "en-US",
|
||||
"disabled": 1,
|
||||
"doc_type": "Sales Invoice",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format",
|
||||
"font": "Default",
|
||||
"html": "",
|
||||
"idx": 0,
|
||||
"line_breaks": 1,
|
||||
"modified": "2020-10-23 19:54:40.634936",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST E-Invoice",
|
||||
"owner": "Administrator",
|
||||
"print_format_builder": 0,
|
||||
"print_format_type": "Jinja",
|
||||
"raw_printing": 0,
|
||||
"show_section_headings": 1,
|
||||
"standard": "Yes"
|
||||
}
|
File diff suppressed because one or more lines are too long
@ -128,6 +128,7 @@ class ReceivablePayableReport(object):
|
||||
credit_note_in_account_currency=0.0,
|
||||
outstanding_in_account_currency=0.0,
|
||||
)
|
||||
self.get_invoices(ple)
|
||||
|
||||
if self.filters.get("group_by_party"):
|
||||
self.init_subtotal_row(ple.party)
|
||||
|
@ -43,7 +43,7 @@ def get_columns():
|
||||
"options": "Account",
|
||||
"width": 170,
|
||||
},
|
||||
{"label": _("Amount"), "fieldname": "amount", "width": 120},
|
||||
{"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120},
|
||||
]
|
||||
|
||||
return columns
|
||||
|
@ -425,7 +425,7 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(totals, "opening", gle)
|
||||
update_value_in_dict(totals, "closing", gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries):
|
||||
if not group_by_voucher_consolidated:
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "total", gle)
|
||||
update_value_in_dict(gle_map[group_by_value].totals, "closing", gle)
|
||||
|
@ -179,7 +179,7 @@ def get_sales_invoice_data(filters):
|
||||
def get_mode_of_payments(filters):
|
||||
mode_of_payments = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list)
|
||||
invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop = frappe.db.sql(
|
||||
"""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
|
||||
@ -200,7 +200,7 @@ def get_mode_of_payments(filters):
|
||||
from `tabJournal Entry` a, `tabJournal Entry Account` b
|
||||
where a.name = b.parent
|
||||
and a.docstatus = 1
|
||||
and b.reference_type = "Sales Invoice"
|
||||
and b.reference_type = 'Sales Invoice'
|
||||
and b.reference_name in ({invoice_list_names})
|
||||
""".format(
|
||||
invoice_list_names=invoice_list_names
|
||||
@ -228,7 +228,7 @@ def get_invoices(filters):
|
||||
def get_mode_of_payment_details(filters):
|
||||
mode_of_payment_details = {}
|
||||
invoice_list = get_invoices(filters)
|
||||
invoice_list_names = ",".join('"' + invoice["name"] + '"' for invoice in invoice_list)
|
||||
invoice_list_names = ",".join("'" + invoice["name"] + "'" for invoice in invoice_list)
|
||||
if invoice_list:
|
||||
inv_mop_detail = frappe.db.sql(
|
||||
"""
|
||||
@ -259,7 +259,7 @@ def get_mode_of_payment_details(filters):
|
||||
from `tabJournal Entry` a, `tabJournal Entry Account` b
|
||||
where a.name = b.parent
|
||||
and a.docstatus = 1
|
||||
and b.reference_type = "Sales Invoice"
|
||||
and b.reference_type = 'Sales Invoice'
|
||||
and b.reference_name in ({invoice_list_names})
|
||||
group by a.owner, a.posting_date, mode_of_payment
|
||||
) t
|
||||
|
@ -2,7 +2,6 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import itertools
|
||||
from json import loads
|
||||
from typing import TYPE_CHECKING, List, Optional, Tuple
|
||||
|
||||
@ -10,8 +9,20 @@ import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.query_builder import AliasedQuery, Criterion, Table
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.utils import DocType
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||
from frappe.utils import (
|
||||
cint,
|
||||
create_batch,
|
||||
cstr,
|
||||
flt,
|
||||
formatdate,
|
||||
get_number_format_info,
|
||||
getdate,
|
||||
now,
|
||||
nowdate,
|
||||
)
|
||||
from pypika import Order
|
||||
from pypika.terms import ExistsCriterion
|
||||
|
||||
@ -428,7 +439,8 @@ def reconcile_against_document(args):
|
||||
# cancel advance entry
|
||||
doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
frappe.flags.ignore_party_validation = True
|
||||
doc.make_gl_entries(cancel=1, adv_adj=1)
|
||||
gl_map = doc.build_gl_map()
|
||||
create_payment_ledger_entry(gl_map, cancel=1, adv_adj=1)
|
||||
|
||||
for entry in entries:
|
||||
check_if_advance_entry_modified(entry)
|
||||
@ -443,7 +455,9 @@ def reconcile_against_document(args):
|
||||
doc.save(ignore_permissions=True)
|
||||
# re-submit advance entry
|
||||
doc = frappe.get_doc(entry.voucher_type, entry.voucher_no)
|
||||
doc.make_gl_entries(cancel=0, adv_adj=1)
|
||||
gl_map = doc.build_gl_map()
|
||||
create_payment_ledger_entry(gl_map, cancel=0, adv_adj=1)
|
||||
|
||||
frappe.flags.ignore_party_validation = False
|
||||
|
||||
if entry.voucher_type in ("Payment Entry", "Journal Entry"):
|
||||
@ -466,7 +480,7 @@ def check_if_advance_entry_modified(args):
|
||||
select t2.{dr_or_cr} from `tabJournal Entry` t1, `tabJournal Entry Account` t2
|
||||
where t1.name = t2.parent and t2.account = %(account)s
|
||||
and t2.party_type = %(party_type)s and t2.party = %(party)s
|
||||
and (t2.reference_type is null or t2.reference_type in ("", "Sales Order", "Purchase Order"))
|
||||
and (t2.reference_type is null or t2.reference_type in ('', 'Sales Order', 'Purchase Order'))
|
||||
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
|
||||
and t1.docstatus=1 """.format(
|
||||
dr_or_cr=args.get("dr_or_cr")
|
||||
@ -486,7 +500,7 @@ def check_if_advance_entry_modified(args):
|
||||
t1.name = t2.parent and t1.docstatus = 1
|
||||
and t1.name = %(voucher_no)s and t2.name = %(voucher_detail_no)s
|
||||
and t1.party_type = %(party_type)s and t1.party = %(party)s and t1.{0} = %(account)s
|
||||
and t2.reference_doctype in ("", "Sales Order", "Purchase Order")
|
||||
and t2.reference_doctype in ('', 'Sales Order', 'Purchase Order')
|
||||
and t2.allocated_amount = %(unreconciled_amount)s
|
||||
""".format(
|
||||
party_account_field
|
||||
@ -807,7 +821,11 @@ def get_held_invoices(party_type, party):
|
||||
return held_invoices
|
||||
|
||||
|
||||
def get_outstanding_invoices(party_type, party, account, condition=None, filters=None):
|
||||
def get_outstanding_invoices(
|
||||
party_type, party, account, common_filter=None, min_outstanding=None, max_outstanding=None
|
||||
):
|
||||
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
outstanding_invoices = []
|
||||
precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2
|
||||
|
||||
@ -820,76 +838,30 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
|
||||
else:
|
||||
party_account_type = erpnext.get_party_account_type(party_type)
|
||||
|
||||
if party_account_type == "Receivable":
|
||||
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
|
||||
else:
|
||||
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
|
||||
payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
|
||||
|
||||
held_invoices = get_held_invoices(party_type, party)
|
||||
|
||||
invoice_list = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
voucher_no, voucher_type, posting_date, due_date,
|
||||
ifnull(sum({dr_or_cr}), 0) as invoice_amount,
|
||||
account_currency as currency
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
party_type = %(party_type)s and party = %(party)s
|
||||
and account = %(account)s and {dr_or_cr} > 0
|
||||
and is_cancelled=0
|
||||
{condition}
|
||||
and ((voucher_type = 'Journal Entry'
|
||||
and (against_voucher = '' or against_voucher is null))
|
||||
or (voucher_type not in ('Journal Entry', 'Payment Entry')))
|
||||
group by voucher_type, voucher_no
|
||||
order by posting_date, name""".format(
|
||||
dr_or_cr=dr_or_cr, condition=condition or ""
|
||||
),
|
||||
{
|
||||
"party_type": party_type,
|
||||
"party": party,
|
||||
"account": account,
|
||||
},
|
||||
as_dict=True,
|
||||
)
|
||||
common_filter = common_filter or []
|
||||
common_filter.append(ple.account_type == party_account_type)
|
||||
common_filter.append(ple.account == account)
|
||||
common_filter.append(ple.party_type == party_type)
|
||||
common_filter.append(ple.party == party)
|
||||
|
||||
payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select against_voucher_type, against_voucher,
|
||||
ifnull(sum({payment_dr_or_cr}), 0) as payment_amount
|
||||
from `tabGL Entry`
|
||||
where party_type = %(party_type)s and party = %(party)s
|
||||
and account = %(account)s
|
||||
and {payment_dr_or_cr} > 0
|
||||
and against_voucher is not null and against_voucher != ''
|
||||
and is_cancelled=0
|
||||
group by against_voucher_type, against_voucher
|
||||
""".format(
|
||||
payment_dr_or_cr=payment_dr_or_cr
|
||||
),
|
||||
{"party_type": party_type, "party": party, "account": account},
|
||||
as_dict=True,
|
||||
ple_query = QueryPaymentLedger()
|
||||
invoice_list = ple_query.get_voucher_outstandings(
|
||||
common_filter=common_filter,
|
||||
min_outstanding=min_outstanding,
|
||||
max_outstanding=max_outstanding,
|
||||
get_invoices=True,
|
||||
)
|
||||
|
||||
pe_map = frappe._dict()
|
||||
for d in payment_entries:
|
||||
pe_map.setdefault((d.against_voucher_type, d.against_voucher), d.payment_amount)
|
||||
|
||||
for d in invoice_list:
|
||||
payment_amount = pe_map.get((d.voucher_type, d.voucher_no), 0)
|
||||
outstanding_amount = flt(d.invoice_amount - payment_amount, precision)
|
||||
payment_amount = d.invoice_amount_in_account_currency - d.outstanding_in_account_currency
|
||||
outstanding_amount = d.outstanding_in_account_currency
|
||||
if outstanding_amount > 0.5 / (10**precision):
|
||||
if (
|
||||
filters
|
||||
and filters.get("outstanding_amt_greater_than")
|
||||
and not (
|
||||
outstanding_amount >= filters.get("outstanding_amt_greater_than")
|
||||
and outstanding_amount <= filters.get("outstanding_amt_less_than")
|
||||
)
|
||||
min_outstanding
|
||||
and max_outstanding
|
||||
and not (outstanding_amount >= min_outstanding and outstanding_amount <= max_outstanding)
|
||||
):
|
||||
continue
|
||||
|
||||
@ -900,7 +872,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters
|
||||
"voucher_no": d.voucher_no,
|
||||
"voucher_type": d.voucher_type,
|
||||
"posting_date": d.posting_date,
|
||||
"invoice_amount": flt(d.invoice_amount),
|
||||
"invoice_amount": flt(d.invoice_amount_in_account_currency),
|
||||
"payment_amount": payment_amount,
|
||||
"outstanding_amount": outstanding_amount,
|
||||
"due_date": d.due_date,
|
||||
@ -1149,9 +1121,7 @@ def repost_gle_for_stock_vouchers(
|
||||
|
||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
||||
|
||||
stock_vouchers_iterator = iter(stock_vouchers)
|
||||
|
||||
while stock_vouchers_chunk := list(itertools.islice(stock_vouchers_iterator, GL_REPOSTING_CHUNK)):
|
||||
for stock_vouchers_chunk in create_batch(stock_vouchers, GL_REPOSTING_CHUNK):
|
||||
gle = get_voucherwise_gl_entries(stock_vouchers_chunk, posting_date)
|
||||
|
||||
for voucher_type, voucher_no in stock_vouchers_chunk:
|
||||
@ -1168,12 +1138,14 @@ def repost_gle_for_stock_vouchers(
|
||||
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
|
||||
else:
|
||||
_delete_gl_entries(voucher_type, voucher_no)
|
||||
frappe.db.commit()
|
||||
|
||||
if not frappe.flags.in_test:
|
||||
frappe.db.commit()
|
||||
|
||||
if repost_doc:
|
||||
repost_doc.db_set(
|
||||
"gl_reposting_index",
|
||||
cint(repost_doc.gl_reposting_index) + GL_REPOSTING_CHUNK,
|
||||
cint(repost_doc.gl_reposting_index) + len(stock_vouchers_chunk),
|
||||
)
|
||||
|
||||
|
||||
@ -1380,7 +1352,9 @@ def check_and_delete_linked_reports(report):
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
def create_payment_ledger_entry(
|
||||
gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0
|
||||
):
|
||||
if gl_entries:
|
||||
ple = None
|
||||
|
||||
@ -1438,7 +1412,7 @@ def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
if gle.against_voucher_type
|
||||
else gle.voucher_type,
|
||||
"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
|
||||
"currency": gle.currency,
|
||||
"account_currency": gle.account_currency,
|
||||
"amount": dr_or_cr,
|
||||
"amount_in_account_currency": dr_or_cr_account_currency,
|
||||
"delinked": True if cancel else False,
|
||||
@ -1453,9 +1427,42 @@ def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.flags.adv_adj = adv_adj
|
||||
ple.flags.from_repost = from_repost
|
||||
ple.flags.update_outstanding = update_outstanding
|
||||
ple.submit()
|
||||
|
||||
|
||||
def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party):
|
||||
ple = frappe.qb.DocType("Payment Ledger Entry")
|
||||
vouchers = [frappe._dict({"voucher_type": voucher_type, "voucher_no": voucher_no})]
|
||||
common_filter = []
|
||||
if account:
|
||||
common_filter.append(ple.account == account)
|
||||
|
||||
if party_type:
|
||||
common_filter.append(ple.party_type == party_type)
|
||||
|
||||
if party:
|
||||
common_filter.append(ple.party == party)
|
||||
|
||||
ple_query = QueryPaymentLedger()
|
||||
|
||||
# on cancellation outstanding can be an empty list
|
||||
voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter)
|
||||
if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding:
|
||||
outstanding = voucher_outstanding[0]
|
||||
ref_doc = frappe.get_doc(voucher_type, voucher_no)
|
||||
|
||||
# Didn't use db_set for optimisation purpose
|
||||
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"]
|
||||
frappe.db.set_value(
|
||||
voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"]
|
||||
)
|
||||
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry):
|
||||
if pl_entry:
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
@ -1477,3 +1484,196 @@ def delink_original_entry(pl_entry):
|
||||
)
|
||||
)
|
||||
query.run()
|
||||
|
||||
|
||||
class QueryPaymentLedger(object):
|
||||
"""
|
||||
Helper Class for Querying Payment Ledger Entry
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.ple = qb.DocType("Payment Ledger Entry")
|
||||
|
||||
# query result
|
||||
self.voucher_outstandings = []
|
||||
|
||||
# query filters
|
||||
self.vouchers = []
|
||||
self.common_filter = []
|
||||
self.min_outstanding = None
|
||||
self.max_outstanding = None
|
||||
|
||||
def reset(self):
|
||||
# clear filters
|
||||
self.vouchers.clear()
|
||||
self.common_filter.clear()
|
||||
self.min_outstanding = self.max_outstanding = None
|
||||
|
||||
# clear result
|
||||
self.voucher_outstandings.clear()
|
||||
|
||||
def query_for_outstanding(self):
|
||||
"""
|
||||
Database query to fetch voucher amount and voucher outstanding using Common Table Expression
|
||||
"""
|
||||
|
||||
ple = self.ple
|
||||
|
||||
filter_on_voucher_no = []
|
||||
filter_on_against_voucher_no = []
|
||||
if self.vouchers:
|
||||
voucher_types = set([x.voucher_type for x in self.vouchers])
|
||||
voucher_nos = set([x.voucher_no for x in self.vouchers])
|
||||
|
||||
filter_on_voucher_no.append(ple.voucher_type.isin(voucher_types))
|
||||
filter_on_voucher_no.append(ple.voucher_no.isin(voucher_nos))
|
||||
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_type.isin(voucher_types))
|
||||
filter_on_against_voucher_no.append(ple.against_voucher_no.isin(voucher_nos))
|
||||
|
||||
# build outstanding amount filter
|
||||
filter_on_outstanding_amount = []
|
||||
if self.min_outstanding:
|
||||
if self.min_outstanding > 0:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency >= self.min_outstanding
|
||||
)
|
||||
else:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency <= self.min_outstanding
|
||||
)
|
||||
if self.max_outstanding:
|
||||
if self.max_outstanding > 0:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency <= self.max_outstanding
|
||||
)
|
||||
else:
|
||||
filter_on_outstanding_amount.append(
|
||||
Table("outstanding").amount_in_account_currency >= self.max_outstanding
|
||||
)
|
||||
|
||||
# build query for voucher amount
|
||||
query_voucher_amount = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.voucher_type,
|
||||
ple.voucher_no,
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.groupby(ple.voucher_type, ple.voucher_no, ple.party_type, ple.party)
|
||||
)
|
||||
|
||||
# build query for voucher outstanding
|
||||
query_voucher_outstanding = (
|
||||
qb.from_(ple)
|
||||
.select(
|
||||
ple.account,
|
||||
ple.against_voucher_type.as_("voucher_type"),
|
||||
ple.against_voucher_no.as_("voucher_no"),
|
||||
ple.party_type,
|
||||
ple.party,
|
||||
ple.posting_date,
|
||||
ple.due_date,
|
||||
ple.account_currency.as_("currency"),
|
||||
Sum(ple.amount).as_("amount"),
|
||||
Sum(ple.amount_in_account_currency).as_("amount_in_account_currency"),
|
||||
)
|
||||
.where(ple.delinked == 0)
|
||||
.where(Criterion.all(filter_on_against_voucher_no))
|
||||
.where(Criterion.all(self.common_filter))
|
||||
.groupby(ple.against_voucher_type, ple.against_voucher_no, ple.party_type, ple.party)
|
||||
)
|
||||
|
||||
# build CTE for combining voucher amount and outstanding
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
qb.with_(query_voucher_amount, "vouchers")
|
||||
.with_(query_voucher_outstanding, "outstanding")
|
||||
.from_(AliasedQuery("vouchers"))
|
||||
.left_join(AliasedQuery("outstanding"))
|
||||
.on(
|
||||
(AliasedQuery("vouchers").account == AliasedQuery("outstanding").account)
|
||||
& (AliasedQuery("vouchers").voucher_type == AliasedQuery("outstanding").voucher_type)
|
||||
& (AliasedQuery("vouchers").voucher_no == AliasedQuery("outstanding").voucher_no)
|
||||
& (AliasedQuery("vouchers").party_type == AliasedQuery("outstanding").party_type)
|
||||
& (AliasedQuery("vouchers").party == AliasedQuery("outstanding").party)
|
||||
)
|
||||
.select(
|
||||
Table("vouchers").account,
|
||||
Table("vouchers").voucher_type,
|
||||
Table("vouchers").voucher_no,
|
||||
Table("vouchers").party_type,
|
||||
Table("vouchers").party,
|
||||
Table("vouchers").posting_date,
|
||||
Table("vouchers").amount.as_("invoice_amount"),
|
||||
Table("vouchers").amount_in_account_currency.as_("invoice_amount_in_account_currency"),
|
||||
Table("outstanding").amount.as_("outstanding"),
|
||||
Table("outstanding").amount_in_account_currency.as_("outstanding_in_account_currency"),
|
||||
(Table("vouchers").amount - Table("outstanding").amount).as_("paid_amount"),
|
||||
(
|
||||
Table("vouchers").amount_in_account_currency - Table("outstanding").amount_in_account_currency
|
||||
).as_("paid_amount_in_account_currency"),
|
||||
Table("vouchers").due_date,
|
||||
Table("vouchers").currency,
|
||||
)
|
||||
.where(Criterion.all(filter_on_outstanding_amount))
|
||||
)
|
||||
|
||||
# build CTE filter
|
||||
# only fetch invoices
|
||||
if self.get_invoices:
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
self.cte_query_voucher_amount_and_outstanding.having(
|
||||
qb.Field("outstanding_in_account_currency") > 0
|
||||
)
|
||||
)
|
||||
# only fetch payments
|
||||
elif self.get_payments:
|
||||
self.cte_query_voucher_amount_and_outstanding = (
|
||||
self.cte_query_voucher_amount_and_outstanding.having(
|
||||
qb.Field("outstanding_in_account_currency") < 0
|
||||
)
|
||||
)
|
||||
|
||||
# execute SQL
|
||||
self.voucher_outstandings = self.cte_query_voucher_amount_and_outstanding.run(as_dict=True)
|
||||
|
||||
def get_voucher_outstandings(
|
||||
self,
|
||||
vouchers=None,
|
||||
common_filter=None,
|
||||
min_outstanding=None,
|
||||
max_outstanding=None,
|
||||
get_payments=False,
|
||||
get_invoices=False,
|
||||
):
|
||||
"""
|
||||
Fetch voucher amount and outstanding amount from Payment Ledger using Database CTE
|
||||
|
||||
vouchers - dict of vouchers to get
|
||||
common_filter - array of criterions
|
||||
min_outstanding - filter on minimum total outstanding amount
|
||||
max_outstanding - filter on maximum total outstanding amount
|
||||
get_invoices - only fetch vouchers(ledger entries with +ve outstanding)
|
||||
get_payments - only fetch payments(ledger entries with -ve outstanding)
|
||||
"""
|
||||
|
||||
self.reset()
|
||||
self.vouchers = vouchers
|
||||
self.common_filter = common_filter or []
|
||||
self.min_outstanding = min_outstanding
|
||||
self.max_outstanding = max_outstanding
|
||||
self.get_payments = get_payments
|
||||
self.get_invoices = get_invoices
|
||||
self.query_for_outstanding()
|
||||
|
||||
return self.voucher_outstandings
|
||||
|
@ -5,7 +5,7 @@
|
||||
"label": "Profit and Loss"
|
||||
}
|
||||
],
|
||||
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Goods and Services Tax (GST India)\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]",
|
||||
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Accounts\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Profit and Loss\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Chart of Accounts\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Purchase Invoice\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Journal Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Payment Entry\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Receivable\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"General Ledger\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Trial Balance\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports & Masters</b></span>\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting Masters\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"General Ledger\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Receivable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounts Payable\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Financial Statements\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Multi Currency\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Bank Statement\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Subscription Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Share Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Cost Center and Budgeting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Opening and Closing\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Taxes\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Profitability\",\"col\":4}}]",
|
||||
"creation": "2020-03-02 15:41:59.515192",
|
||||
"docstatus": 0,
|
||||
"doctype": "Workspace",
|
||||
@ -777,147 +777,6 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Goods and Services Tax (GST India)",
|
||||
"link_count": 0,
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Card Break"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "GST Settings",
|
||||
"link_count": 0,
|
||||
"link_to": "GST Settings",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "GST HSN Code",
|
||||
"link_count": 0,
|
||||
"link_to": "GST HSN Code",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "GSTR-1",
|
||||
"link_count": 0,
|
||||
"link_to": "GSTR-1",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "GSTR-2",
|
||||
"link_count": 0,
|
||||
"link_to": "GSTR-2",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "GSTR 3B Report",
|
||||
"link_count": 0,
|
||||
"link_to": "GSTR 3B Report",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "GST Sales Register",
|
||||
"link_count": 0,
|
||||
"link_to": "GST Sales Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "GST Purchase Register",
|
||||
"link_count": 0,
|
||||
"link_to": "GST Purchase Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "GST Itemised Sales Register",
|
||||
"link_count": 0,
|
||||
"link_to": "GST Itemised Sales Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "GST Itemised Purchase Register",
|
||||
"link_count": 0,
|
||||
"link_to": "GST Itemised Purchase Register",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "C-Form",
|
||||
"link_count": 0,
|
||||
"link_to": "C-Form",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "",
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Lower Deduction Certificate",
|
||||
"link_count": 0,
|
||||
"link_to": "Lower Deduction Certificate",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@ -1159,6 +1018,17 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "Lower Deduction Certificate",
|
||||
"link_count": 0,
|
||||
"link_to": "Lower Deduction Certificate",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "India",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@ -1223,7 +1093,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-06-10 15:49:42.990860",
|
||||
"modified": "2022-06-24 05:41:09.236458",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
|
@ -252,6 +252,7 @@ class Asset(AccountsController):
|
||||
number_of_pending_depreciations += 1
|
||||
|
||||
skip_row = False
|
||||
should_get_last_day = is_last_day_of_the_month(finance_book.depreciation_start_date)
|
||||
|
||||
for n in range(start[finance_book.idx - 1], number_of_pending_depreciations):
|
||||
# If depreciation is already completed (for double declining balance)
|
||||
@ -265,6 +266,9 @@ class Asset(AccountsController):
|
||||
finance_book.depreciation_start_date, n * cint(finance_book.frequency_of_depreciation)
|
||||
)
|
||||
|
||||
if should_get_last_day:
|
||||
schedule_date = get_last_day(schedule_date)
|
||||
|
||||
# schedule date will be a year later from start date
|
||||
# so monthly schedule date is calculated by removing 11 months from it
|
||||
monthly_schedule_date = add_months(schedule_date, -finance_book.frequency_of_depreciation + 1)
|
||||
@ -849,14 +853,9 @@ class Asset(AccountsController):
|
||||
if args.get("rate_of_depreciation") and on_validate:
|
||||
return args.get("rate_of_depreciation")
|
||||
|
||||
no_of_years = (
|
||||
flt(args.get("total_number_of_depreciations") * flt(args.get("frequency_of_depreciation")))
|
||||
/ 12
|
||||
)
|
||||
value = flt(args.get("expected_value_after_useful_life")) / flt(self.gross_purchase_amount)
|
||||
|
||||
# square root of flt(salvage_value) / flt(asset_cost)
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(no_of_years, 2))
|
||||
depreciation_rate = math.pow(value, 1.0 / flt(args.get("total_number_of_depreciations"), 2))
|
||||
|
||||
return 100 * (1 - flt(depreciation_rate, float_precision))
|
||||
|
||||
@ -1105,9 +1104,18 @@ def is_cwip_accounting_enabled(asset_category):
|
||||
def get_total_days(date, frequency):
|
||||
period_start_date = add_months(date, cint(frequency) * -1)
|
||||
|
||||
if is_last_day_of_the_month(date):
|
||||
period_start_date = get_last_day(period_start_date)
|
||||
|
||||
return date_diff(date, period_start_date)
|
||||
|
||||
|
||||
def is_last_day_of_the_month(date):
|
||||
last_day_of_the_month = get_last_day(date)
|
||||
|
||||
return getdate(last_day_of_the_month) == getdate(date)
|
||||
|
||||
|
||||
@erpnext.allow_regional
|
||||
def get_depreciation_amount(asset, depreciable_value, row):
|
||||
if row.depreciation_method in ("Straight Line", "Manual"):
|
||||
|
@ -707,35 +707,26 @@ class TestDepreciationMethods(AssetSetup):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
def test_discounted_wdv_depreciation_rate_for_indian_region(self):
|
||||
# set indian company
|
||||
company_flag = frappe.flags.company
|
||||
frappe.flags.company = "_Test Company"
|
||||
|
||||
finance_book = frappe.new_doc("Finance Book")
|
||||
finance_book.finance_book_name = "Income Tax"
|
||||
finance_book.for_income_tax = 1
|
||||
finance_book.insert(ignore_if_duplicate=True)
|
||||
|
||||
def test_monthly_depreciation_by_wdv_method(self):
|
||||
asset = create_asset(
|
||||
calculate_depreciation=1,
|
||||
available_for_use_date="2030-07-12",
|
||||
purchase_date="2030-01-01",
|
||||
finance_book=finance_book.name,
|
||||
available_for_use_date="2022-02-15",
|
||||
purchase_date="2022-02-15",
|
||||
depreciation_method="Written Down Value",
|
||||
expected_value_after_useful_life=12500,
|
||||
depreciation_start_date="2030-12-31",
|
||||
total_number_of_depreciations=3,
|
||||
frequency_of_depreciation=12,
|
||||
gross_purchase_amount=10000,
|
||||
expected_value_after_useful_life=5000,
|
||||
depreciation_start_date="2022-02-28",
|
||||
total_number_of_depreciations=5,
|
||||
frequency_of_depreciation=1,
|
||||
)
|
||||
|
||||
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
|
||||
|
||||
expected_schedules = [
|
||||
["2030-12-31", 11849.32, 11849.32],
|
||||
["2031-12-31", 44075.34, 55924.66],
|
||||
["2032-12-31", 22037.67, 77962.33],
|
||||
["2033-07-12", 9537.67, 87500.0],
|
||||
["2022-02-28", 645.0, 645.0],
|
||||
["2022-03-31", 1206.8, 1851.8],
|
||||
["2022-04-30", 1051.12, 2902.92],
|
||||
["2022-05-31", 915.52, 3818.44],
|
||||
["2022-06-30", 797.42, 4615.86],
|
||||
["2022-07-15", 384.14, 5000.0],
|
||||
]
|
||||
|
||||
schedules = [
|
||||
@ -749,9 +740,6 @@ class TestDepreciationMethods(AssetSetup):
|
||||
|
||||
self.assertEqual(schedules, expected_schedules)
|
||||
|
||||
# reset indian company
|
||||
frappe.flags.company = company_flag
|
||||
|
||||
|
||||
class TestDepreciationBasics(AssetSetup):
|
||||
def test_depreciation_without_pro_rata(self):
|
||||
@ -838,7 +826,7 @@ class TestDepreciationBasics(AssetSetup):
|
||||
expected_values = [["2020-12-31", 30000.0], ["2021-12-31", 30000.0], ["2022-12-31", 30000.0]]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(expected_values[i][0], schedule.schedule_date)
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
|
||||
def test_set_accumulated_depreciation(self):
|
||||
@ -1333,6 +1321,32 @@ class TestDepreciationBasics(AssetSetup):
|
||||
asset.cost_center = "Main - _TC"
|
||||
asset.submit()
|
||||
|
||||
def test_depreciation_on_final_day_of_the_month(self):
|
||||
"""Tests if final day of the month is picked each time, if the depreciation start date is the last day of the month."""
|
||||
|
||||
asset = create_asset(
|
||||
item_code="Macbook Pro",
|
||||
calculate_depreciation=1,
|
||||
purchase_date="2020-01-30",
|
||||
available_for_use_date="2020-02-15",
|
||||
depreciation_start_date="2020-02-29",
|
||||
frequency_of_depreciation=1,
|
||||
total_number_of_depreciations=5,
|
||||
submit=1,
|
||||
)
|
||||
|
||||
expected_dates = [
|
||||
"2020-02-29",
|
||||
"2020-03-31",
|
||||
"2020-04-30",
|
||||
"2020-05-31",
|
||||
"2020-06-30",
|
||||
"2020-07-15",
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_dates[i]), getdate(schedule.schedule_date))
|
||||
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
|
@ -47,17 +47,19 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
|
||||
team_member = frappe.db.get_value("User", assign_to_member, "email")
|
||||
args = {
|
||||
"doctype": "Asset Maintenance",
|
||||
"assign_to": [team_member],
|
||||
"assign_to": team_member,
|
||||
"name": asset_maintenance_name,
|
||||
"description": maintenance_task,
|
||||
"date": next_due_date,
|
||||
}
|
||||
if not frappe.db.sql(
|
||||
"""select owner from `tabToDo`
|
||||
where reference_type=%(doctype)s and reference_name=%(name)s and status="Open"
|
||||
where reference_type=%(doctype)s and reference_name=%(name)s and status='Open'
|
||||
and owner=%(assign_to)s""",
|
||||
args,
|
||||
):
|
||||
# assign_to function expects a list
|
||||
args["assign_to"] = [args["assign_to"]]
|
||||
assign_to.add(args)
|
||||
|
||||
|
||||
|
@ -12,9 +12,6 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
)
|
||||
from erpnext.assets.doctype.asset.asset import get_depreciation_amount
|
||||
from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts
|
||||
from erpnext.regional.india.utils import (
|
||||
get_depreciation_amount as get_depreciation_amount_for_india,
|
||||
)
|
||||
|
||||
|
||||
class AssetValueAdjustment(Document):
|
||||
@ -133,10 +130,7 @@ class AssetValueAdjustment(Document):
|
||||
depreciation_amount = days * rate_per_day
|
||||
from_date = data.schedule_date
|
||||
else:
|
||||
if country == "India":
|
||||
depreciation_amount = get_depreciation_amount_for_india(asset, value_after_depreciation, d)
|
||||
else:
|
||||
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
|
||||
depreciation_amount = get_depreciation_amount(asset, value_after_depreciation, d)
|
||||
|
||||
if depreciation_amount:
|
||||
value_after_depreciation -= flt(depreciation_amount)
|
||||
|
@ -425,7 +425,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
|
||||
company: me.frm.doc.company
|
||||
},
|
||||
allow_child_item_selection: true,
|
||||
child_fielname: "items",
|
||||
child_fieldname: "items",
|
||||
child_columns: ["item_code", "qty"]
|
||||
})
|
||||
}, __("Get Items From"));
|
||||
|
@ -1,3 +0,0 @@
|
||||
{% include "erpnext/regional/india/taxes.js" %}
|
||||
|
||||
erpnext.setup_auto_gst_taxation('Purchase Order');
|
@ -140,6 +140,43 @@ class TestPurchaseOrder(FrappeTestCase):
|
||||
# ordered qty decreases as ordered qty is 0 (deleted row)
|
||||
self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
|
||||
|
||||
def test_supplied_items_validations_on_po_update_after_submit(self):
|
||||
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1, qty=5, rate=100)
|
||||
item = po.items[0]
|
||||
|
||||
original_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
|
||||
# Just update rate
|
||||
trans_item = [
|
||||
{
|
||||
"item_code": "_Test FG Item",
|
||||
"rate": 20,
|
||||
"qty": 5,
|
||||
"conversion_factor": 1.0,
|
||||
"docname": item.name,
|
||||
}
|
||||
]
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(set(original_supplied_items.keys()), set(new_supplied_items.keys()))
|
||||
|
||||
# Update qty to 2x
|
||||
trans_item[0]["qty"] *= 2
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
po.reload()
|
||||
|
||||
new_supplied_items = {po.name: po.required_qty for po in po.supplied_items}
|
||||
self.assertEqual(2 * sum(original_supplied_items.values()), sum(new_supplied_items.values()))
|
||||
|
||||
# Set transfer qty and attempt to update qty, shouldn't be allowed
|
||||
po.supplied_items[0].supplied_qty = 2
|
||||
po.supplied_items[0].db_update()
|
||||
trans_item[0]["qty"] *= 2
|
||||
with self.assertRaises(frappe.ValidationError):
|
||||
update_child_qty_rate("Purchase Order", json.dumps(trans_item), po.name)
|
||||
|
||||
def test_update_child(self):
|
||||
mr = make_material_request(qty=10)
|
||||
po = make_purchase_order(mr.name)
|
||||
|
@ -213,6 +213,7 @@
|
||||
"width": "60px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_uom",
|
||||
"fieldtype": "Link",
|
||||
"label": "Stock UOM",
|
||||
@ -242,6 +243,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "conversion_factor",
|
||||
"fieldtype": "Float",
|
||||
"label": "UOM Conversion Factor",
|
||||
@ -593,6 +595,7 @@
|
||||
"label": "Billed, Received & Returned"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.uom != doc.stock_uom",
|
||||
"fieldname": "stock_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Qty in Stock UOM",
|
||||
@ -851,7 +854,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-02 13:10:18.398976",
|
||||
"modified": "2022-06-17 05:29:40.602349",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
@ -1,3 +0,0 @@
|
||||
{% include "erpnext/regional/india/party.js" %}
|
||||
|
||||
erpnext.setup_gst_reminder_button('Supplier');
|
@ -252,7 +252,7 @@ def get_mapped_pi_records():
|
||||
ON pi_item.`purchase_order` = po.`name`
|
||||
WHERE
|
||||
pi_item.docstatus = 1
|
||||
AND po.status not in ("Closed","Completed","Cancelled")
|
||||
AND po.status not in ('Closed','Completed','Cancelled')
|
||||
AND pi_item.po_detail IS NOT NULL
|
||||
"""
|
||||
)
|
||||
@ -271,7 +271,7 @@ def get_mapped_pr_records():
|
||||
pr.docstatus=1
|
||||
AND pr.name=pr_item.parent
|
||||
AND pr_item.purchase_order_item IS NOT NULL
|
||||
AND pr.status not in ("Closed","Completed","Cancelled")
|
||||
AND pr.status not in ('Closed','Completed','Cancelled')
|
||||
"""
|
||||
)
|
||||
)
|
||||
@ -302,7 +302,7 @@ def get_po_entries(conditions):
|
||||
WHERE
|
||||
parent.docstatus = 1
|
||||
AND parent.name = child.parent
|
||||
AND parent.status not in ("Closed","Completed","Cancelled")
|
||||
AND parent.status not in ('Closed','Completed','Cancelled')
|
||||
{conditions}
|
||||
GROUP BY
|
||||
parent.name, child.item_code
|
||||
|
@ -59,6 +59,7 @@ frappe.query_reports["Purchase Order Analysis"] = {
|
||||
for (let option of status){
|
||||
options.push({
|
||||
"value": option,
|
||||
"label": __(option),
|
||||
"description": ""
|
||||
})
|
||||
}
|
||||
|
@ -46,6 +46,7 @@ from erpnext.controllers.print_settings import (
|
||||
from erpnext.controllers.sales_and_purchase_return import validate_return
|
||||
from erpnext.exceptions import InvalidCurrency
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.stock.doctype.item.item import get_uom_conv_factor
|
||||
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
|
||||
from erpnext.stock.get_item_details import (
|
||||
_get_item_tax_template,
|
||||
@ -548,6 +549,15 @@ class AccountsController(TransactionBase):
|
||||
if ret.get("pricing_rules"):
|
||||
self.apply_pricing_rule_on_items(item, ret)
|
||||
self.set_pricing_rule_details(item, ret)
|
||||
else:
|
||||
# Transactions line item without item code
|
||||
|
||||
uom = item.get("uom")
|
||||
stock_uom = item.get("stock_uom")
|
||||
if bool(uom) != bool(stock_uom): # xor
|
||||
item.stock_uom = item.uom = uom or stock_uom
|
||||
|
||||
item.conversion_factor = get_uom_conv_factor(item.get("uom"), item.get("stock_uom"))
|
||||
|
||||
if self.doctype == "Purchase Invoice":
|
||||
self.set_expense_account(for_validate)
|
||||
@ -1838,6 +1848,17 @@ class AccountsController(TransactionBase):
|
||||
jv.save()
|
||||
jv.submit()
|
||||
|
||||
def check_conversion_rate(self):
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if not default_currency:
|
||||
throw(_("Please enter default currency in Company Master"))
|
||||
if (
|
||||
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
|
||||
or not self.conversion_rate
|
||||
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
|
||||
):
|
||||
throw(_("Conversion rate cannot be 0 or 1"))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_tax_rate(account_head):
|
||||
@ -2039,7 +2060,7 @@ def get_advance_journal_entries(
|
||||
journal_entries = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Journal Entry" as reference_type, t1.name as reference_name,
|
||||
'Journal Entry' as reference_type, t1.name as reference_name,
|
||||
t1.remark as remarks, t2.{0} as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t2.exchange_rate
|
||||
from
|
||||
@ -2094,7 +2115,7 @@ def get_advance_payment_entries(
|
||||
payment_entries_against_order = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
"Payment Entry" as reference_type, t1.name as reference_name,
|
||||
'Payment Entry' as reference_type, t1.name as reference_name,
|
||||
t1.remarks, t2.allocated_amount as amount, t2.name as reference_row,
|
||||
t2.reference_name as against_order, t1.posting_date,
|
||||
t1.{0} as currency, t1.{4} as exchange_rate
|
||||
@ -2114,7 +2135,7 @@ def get_advance_payment_entries(
|
||||
if include_unallocated:
|
||||
unallocated_payment_entries = frappe.db.sql(
|
||||
"""
|
||||
select "Payment Entry" as reference_type, name as reference_name, posting_date,
|
||||
select 'Payment Entry' as reference_type, name as reference_name, posting_date,
|
||||
remarks, unallocated_amount as amount, {2} as exchange_rate, {3} as currency
|
||||
from `tabPayment Entry`
|
||||
where
|
||||
@ -2419,7 +2440,7 @@ def update_bin_on_delete(row, doctype):
|
||||
update_bin_qty(row.item_code, row.warehouse, qty_dict)
|
||||
|
||||
|
||||
def validate_and_delete_children(parent, data):
|
||||
def validate_and_delete_children(parent, data) -> bool:
|
||||
deleted_children = []
|
||||
updated_item_names = [d.get("docname") for d in data]
|
||||
for item in parent.items:
|
||||
@ -2438,6 +2459,8 @@ def validate_and_delete_children(parent, data):
|
||||
for d in deleted_children:
|
||||
update_bin_on_delete(d, parent.doctype)
|
||||
|
||||
return bool(deleted_children)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
|
||||
@ -2501,13 +2524,38 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
):
|
||||
frappe.throw(_("Cannot set quantity less than received quantity"))
|
||||
|
||||
def should_update_supplied_items(doc) -> bool:
|
||||
"""Subcontracted PO can allow following changes *after submit*:
|
||||
|
||||
1. Change rate of subcontracting - regardless of other changes.
|
||||
2. Change qty and/or add new items and/or remove items
|
||||
Exception: Transfer/Consumption is already made, qty change not allowed.
|
||||
"""
|
||||
|
||||
supplied_items_processed = any(
|
||||
item.supplied_qty or item.consumed_qty or item.returned_qty for item in doc.supplied_items
|
||||
)
|
||||
|
||||
update_supplied_items = (
|
||||
any_qty_changed or items_added_or_removed or any_conversion_factor_changed
|
||||
)
|
||||
if update_supplied_items and supplied_items_processed:
|
||||
frappe.throw(_("Item qty can not be updated as raw materials are already processed."))
|
||||
|
||||
return update_supplied_items
|
||||
|
||||
data = json.loads(trans_items)
|
||||
|
||||
any_qty_changed = False # updated to true if any item's qty changes
|
||||
items_added_or_removed = False # updated to true if any new item is added or removed
|
||||
any_conversion_factor_changed = False
|
||||
|
||||
sales_doctypes = ["Sales Order", "Sales Invoice", "Delivery Note", "Quotation"]
|
||||
parent = frappe.get_doc(parent_doctype, parent_doctype_name)
|
||||
|
||||
check_doc_permissions(parent, "write")
|
||||
validate_and_delete_children(parent, data)
|
||||
_removed_items = validate_and_delete_children(parent, data)
|
||||
items_added_or_removed |= _removed_items
|
||||
|
||||
for d in data:
|
||||
new_child_flag = False
|
||||
@ -2518,6 +2566,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
|
||||
if not d.get("docname"):
|
||||
new_child_flag = True
|
||||
items_added_or_removed = True
|
||||
check_doc_permissions(parent, "create")
|
||||
child_item = get_new_child_item(d)
|
||||
else:
|
||||
@ -2540,6 +2589,7 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
qty_unchanged = prev_qty == new_qty
|
||||
uom_unchanged = prev_uom == new_uom
|
||||
conversion_factor_unchanged = prev_con_fac == new_con_fac
|
||||
any_conversion_factor_changed |= not conversion_factor_unchanged
|
||||
date_unchanged = (
|
||||
prev_date == getdate(new_date) if prev_date and new_date else False
|
||||
) # in case of delivery note etc
|
||||
@ -2553,6 +2603,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
continue
|
||||
|
||||
validate_quantity(child_item, d)
|
||||
if flt(child_item.get("qty")) != flt(d.get("qty")):
|
||||
any_qty_changed = True
|
||||
|
||||
child_item.qty = flt(d.get("qty"))
|
||||
rate_precision = child_item.precision("rate") or 2
|
||||
@ -2658,8 +2710,9 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
if parent.is_subcontracted:
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
if should_update_supplied_items(parent):
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
parent.save()
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
|
@ -300,7 +300,7 @@ class BuyingController(StockController, Subcontracting):
|
||||
raise_error_if_no_rate=False,
|
||||
)
|
||||
|
||||
rate = flt(outgoing_rate * d.conversion_factor, d.precision("rate"))
|
||||
rate = flt(outgoing_rate * (d.conversion_factor or 1), d.precision("rate"))
|
||||
else:
|
||||
rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), "rate")
|
||||
|
||||
|
@ -29,8 +29,8 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
or employee_name like %(txt)s)
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, employee_name), locate(%(_txt)s, employee_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, employee_name) > 0 then locate(%(_txt)s, employee_name) else 99999 end),
|
||||
idx desc,
|
||||
name, employee_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
@ -60,9 +60,9 @@ def lead_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
or company_name like %(txt)s)
|
||||
{mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, lead_name), locate(%(_txt)s, lead_name), 99999),
|
||||
if(locate(%(_txt)s, company_name), locate(%(_txt)s, company_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, lead_name) > 0 then locate(%(_txt)s, lead_name) else 99999 end),
|
||||
(case when locate(%(_txt)s, company_name) > 0 then locate(%(_txt)s, company_name) else 99999 end),
|
||||
idx desc,
|
||||
name, lead_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
@ -96,8 +96,8 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
and ({scond}) and disabled=0
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, customer_name), locate(%(_txt)s, customer_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, customer_name) > 0 then locate(%(_txt)s, customer_name) else 99999 end),
|
||||
idx desc,
|
||||
name, customer_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
@ -130,11 +130,11 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
where docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or supplier_name like %(txt)s) and disabled=0
|
||||
and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
|
||||
and (on_hold = 0 or (on_hold = 1 and CURRENT_DATE > release_date))
|
||||
{mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
if(locate(%(_txt)s, supplier_name), locate(%(_txt)s, supplier_name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
(case when locate(%(_txt)s, supplier_name) > 0 then locate(%(_txt)s, supplier_name) else 99999 end),
|
||||
idx desc,
|
||||
name, supplier_name
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
@ -305,15 +305,15 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields}
|
||||
from tabBOM
|
||||
where tabBOM.docstatus=1
|
||||
and tabBOM.is_active=1
|
||||
and tabBOM.`{key}` like %(txt)s
|
||||
from `tabBOM`
|
||||
where `tabBOM`.docstatus=1
|
||||
and `tabBOM`.is_active=1
|
||||
and `tabBOM`.`{key}` like %(txt)s
|
||||
{fcond} {mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
(case when locate(%(_txt)s, name) > 0 then locate(%(_txt)s, name) else 99999 end),
|
||||
idx desc, name
|
||||
limit %(start)s, %(page_len)s """.format(
|
||||
limit %(page_len)s offset %(start)s""".format(
|
||||
fields=", ".join(fields),
|
||||
fcond=get_filters_cond(doctype, filters, conditions).replace("%", "%%"),
|
||||
mcond=get_match_cond(doctype).replace("%", "%%"),
|
||||
@ -340,16 +340,16 @@ def get_project_name(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
fields = get_fields("Project", ["name", "project_name"])
|
||||
searchfields = frappe.get_meta("Project").get_search_fields()
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
searchfields = " or ".join(["`tabProject`." + field + " like %(txt)s" for field in searchfields])
|
||||
|
||||
return frappe.db.sql(
|
||||
"""select {fields} from `tabProject`
|
||||
where
|
||||
`tabProject`.status not in ("Completed", "Cancelled")
|
||||
`tabProject`.status not in ('Completed', 'Cancelled')
|
||||
and {cond} {scond} {match_cond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
idx desc,
|
||||
(case when locate(%(_txt)s, `tabProject`.name) > 0 then locate(%(_txt)s, `tabProject`.name) else 99999 end),
|
||||
`tabProject`.idx desc,
|
||||
`tabProject`.name asc
|
||||
limit {page_len} offset {start}""".format(
|
||||
fields=", ".join(["`tabProject`.{0}".format(f) for f in fields]),
|
||||
@ -374,7 +374,7 @@ def get_delivery_notes_to_be_billed(doctype, txt, searchfield, start, page_len,
|
||||
from `tabDelivery Note`
|
||||
where `tabDelivery Note`.`%(key)s` like %(txt)s and
|
||||
`tabDelivery Note`.docstatus = 1
|
||||
and status not in ("Stopped", "Closed") %(fcond)s
|
||||
and status not in ('Stopped', 'Closed') %(fcond)s
|
||||
and (
|
||||
(`tabDelivery Note`.is_return = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
or (`tabDelivery Note`.grand_total = 0 and `tabDelivery Note`.per_billed < 100)
|
||||
@ -654,7 +654,7 @@ def warehouse_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
filter_dict = get_doctype_wise_filters(filters)
|
||||
|
||||
query = """select `tabWarehouse`.name,
|
||||
CONCAT_WS(" : ", "Actual Qty", ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
|
||||
CONCAT_WS(' : ', 'Actual Qty', ifnull(round(`tabBin`.actual_qty, 2), 0 )) actual_qty
|
||||
from `tabWarehouse` left join `tabBin`
|
||||
on `tabBin`.warehouse = `tabWarehouse`.name {bin_conditions}
|
||||
where
|
||||
|
@ -352,9 +352,9 @@ class StatusUpdater(Document):
|
||||
for args in self.status_updater:
|
||||
# condition to include current record (if submit or no if cancel)
|
||||
if self.docstatus == 1:
|
||||
args["cond"] = ' or parent="%s"' % self.name.replace('"', '"')
|
||||
args["cond"] = " or parent='%s'" % self.name.replace('"', '"')
|
||||
else:
|
||||
args["cond"] = ' and parent!="%s"' % self.name.replace('"', '"')
|
||||
args["cond"] = " and parent!='%s'" % self.name.replace('"', '"')
|
||||
|
||||
self._update_children(args, update_modified)
|
||||
|
||||
@ -384,7 +384,7 @@ class StatusUpdater(Document):
|
||||
args["second_source_condition"] = frappe.db.sql(
|
||||
""" select ifnull((select sum(%(second_source_field)s)
|
||||
from `tab%(second_source_dt)s`
|
||||
where `%(second_join_field)s`="%(detail_id)s"
|
||||
where `%(second_join_field)s`='%(detail_id)s'
|
||||
and (`tab%(second_source_dt)s`.docstatus=1)
|
||||
%(second_source_extra_cond)s), 0) """
|
||||
% args
|
||||
@ -398,7 +398,7 @@ class StatusUpdater(Document):
|
||||
frappe.db.sql(
|
||||
"""
|
||||
(select ifnull(sum(%(source_field)s), 0)
|
||||
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
|
||||
from `tab%(source_dt)s` where `%(join_field)s`='%(detail_id)s'
|
||||
and (docstatus=1 %(cond)s) %(extra_cond)s)
|
||||
"""
|
||||
% args
|
||||
@ -443,9 +443,9 @@ class StatusUpdater(Document):
|
||||
"""update `tab%(target_parent_dt)s`
|
||||
set %(target_parent_field)s = round(
|
||||
ifnull((select
|
||||
ifnull(sum(if(abs(%(target_ref_field)s) > abs(%(target_field)s), abs(%(target_field)s), abs(%(target_ref_field)s))), 0)
|
||||
ifnull(sum(case when abs(%(target_ref_field)s) > abs(%(target_field)s) then abs(%(target_field)s) else abs(%(target_ref_field)s) end), 0)
|
||||
/ sum(abs(%(target_ref_field)s)) * 100
|
||||
from `tab%(target_dt)s` where parent="%(name)s" having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||
from `tab%(target_dt)s` where parent='%(name)s' having sum(abs(%(target_ref_field)s)) > 0), 0), 6)
|
||||
%(update_modified)s
|
||||
where name='%(name)s'"""
|
||||
% args
|
||||
@ -455,9 +455,9 @@ class StatusUpdater(Document):
|
||||
if args.get("status_field"):
|
||||
frappe.db.sql(
|
||||
"""update `tab%(target_parent_dt)s`
|
||||
set %(status_field)s = if(%(target_parent_field)s<0.001,
|
||||
'Not %(keyword)s', if(%(target_parent_field)s>=99.999999,
|
||||
'Fully %(keyword)s', 'Partly %(keyword)s'))
|
||||
set %(status_field)s = (case when %(target_parent_field)s<0.001 then 'Not %(keyword)s'
|
||||
else case when %(target_parent_field)s>=99.999999 then 'Fully %(keyword)s'
|
||||
else 'Partly %(keyword)s' end end)
|
||||
where name='%(name)s'"""
|
||||
% args
|
||||
)
|
||||
|
@ -166,7 +166,7 @@ class StockController(AccountsController):
|
||||
"against": warehouse_account[sle.warehouse]["account"],
|
||||
"cost_center": item_row.cost_center,
|
||||
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
|
||||
"credit": flt(sle.stock_value_difference, precision),
|
||||
"debit": -1 * flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
|
||||
},
|
||||
|
@ -500,6 +500,9 @@ class calculate_taxes_and_totals(object):
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
if self.doc.apply_discount_on == "Grand Total" and self.doc.get("is_cash_or_non_trade_discount"):
|
||||
self.doc.grand_total -= self.doc.discount_amount
|
||||
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(
|
||||
self.doc.grand_total - self.doc.net_total - flt(self.doc.rounding_adjustment),
|
||||
@ -594,6 +597,12 @@ class calculate_taxes_and_totals(object):
|
||||
if not self.doc.apply_discount_on:
|
||||
frappe.throw(_("Please select Apply Discount On"))
|
||||
|
||||
if self.doc.apply_discount_on == "Grand Total" and self.doc.get(
|
||||
"is_cash_or_non_trade_discount"
|
||||
):
|
||||
self.discount_amount_applied = True
|
||||
return
|
||||
|
||||
self.doc.base_discount_amount = flt(
|
||||
self.doc.discount_amount * self.doc.conversion_rate, self.doc.precision("base_discount_amount")
|
||||
)
|
||||
|
48
erpnext/crm/doctype/crm_note/crm_note.json
Normal file
48
erpnext/crm/doctype/crm_note/crm_note.json
Normal file
@ -0,0 +1,48 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-06-04 15:49:23.416644",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"note",
|
||||
"added_by",
|
||||
"added_on"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 5,
|
||||
"fieldname": "note",
|
||||
"fieldtype": "Text Editor",
|
||||
"in_list_view": 1,
|
||||
"label": "Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "added_by",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Added By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "added_on",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Added On"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-06-04 16:29:07.807252",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Note",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1,10 +1,9 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class EInvoiceUser(Document):
|
||||
class CRMNote(Document):
|
||||
pass
|
@ -10,12 +10,10 @@
|
||||
"campaign_naming_by",
|
||||
"allow_lead_duplication_based_on_emails",
|
||||
"column_break_4",
|
||||
"create_event_on_next_contact_date",
|
||||
"auto_creation_of_contact",
|
||||
"opportunity_section",
|
||||
"close_opportunity_after_days",
|
||||
"column_break_9",
|
||||
"create_event_on_next_contact_date_opportunity",
|
||||
"quotation_section",
|
||||
"default_valid_till",
|
||||
"section_break_13",
|
||||
@ -55,12 +53,6 @@
|
||||
"fieldtype": "Check",
|
||||
"label": "Auto Creation of Contact"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_event_on_next_contact_date",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Event on Next Contact Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity_section",
|
||||
"fieldtype": "Section Break",
|
||||
@ -73,12 +65,6 @@
|
||||
"fieldtype": "Int",
|
||||
"label": "Close Replied Opportunity After Days"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "create_event_on_next_contact_date_opportunity",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create Event on Next Contact Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
@ -105,7 +91,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-12-20 12:51:38.894252",
|
||||
"modified": "2022-06-06 11:22:08.464253",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "CRM Settings",
|
||||
@ -143,5 +129,6 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -24,31 +24,39 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
this.frm.set_query("lead_owner", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" }
|
||||
});
|
||||
|
||||
this.frm.set_query("contact_by", function (doc, cdt, cdn) {
|
||||
return { query: "frappe.core.doctype.user.user.user_query" }
|
||||
});
|
||||
}
|
||||
|
||||
refresh () {
|
||||
var me = this;
|
||||
let doc = this.frm.doc;
|
||||
erpnext.toggle_naming_series();
|
||||
frappe.dynamic_link = { doc: doc, fieldname: 'name', doctype: 'Lead' }
|
||||
frappe.dynamic_link = {
|
||||
doc: doc,
|
||||
fieldname: 'name',
|
||||
doctype: 'Lead'
|
||||
};
|
||||
|
||||
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
|
||||
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), this.make_opportunity, __("Create"));
|
||||
this.frm.add_custom_button(__("Opportunity"), function() {
|
||||
me.frm.trigger("make_opportunity");
|
||||
}, __("Create"));
|
||||
this.frm.add_custom_button(__("Quotation"), this.make_quotation, __("Create"));
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||
this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
|
||||
if (!doc.__onload.linked_prospects.length) {
|
||||
this.frm.add_custom_button(__("Prospect"), this.make_prospect, __("Create"));
|
||||
this.frm.add_custom_button(__('Add to Prospect'), this.add_lead_to_prospect, __('Action'));
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(this.frm);
|
||||
cur_frm.trigger('render_contact_day_html');
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(this.frm);
|
||||
}
|
||||
|
||||
this.frm.dashboard.links_area.hide();
|
||||
this.show_notes();
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
add_lead_to_prospect () {
|
||||
@ -74,7 +82,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __('...Adding Lead to Prospect')
|
||||
freeze_message: __('Adding Lead to Prospect...')
|
||||
});
|
||||
}, __('Add Lead to Prospect'), __('Add'));
|
||||
}
|
||||
@ -86,13 +94,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
})
|
||||
}
|
||||
|
||||
make_opportunity () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
make_quotation () {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_quotation",
|
||||
@ -111,9 +112,10 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
prospect.fax = cur_frm.doc.fax;
|
||||
prospect.website = cur_frm.doc.website;
|
||||
prospect.prospect_owner = cur_frm.doc.lead_owner;
|
||||
prospect.notes = cur_frm.doc.notes;
|
||||
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
|
||||
lead_prospect_row.lead = cur_frm.doc.name;
|
||||
let leads_row = frappe.model.add_child(prospect, 'leads');
|
||||
leads_row.lead = cur_frm.doc.name;
|
||||
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
});
|
||||
@ -125,26 +127,109 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
|
||||
}
|
||||
}
|
||||
|
||||
contact_date () {
|
||||
if (this.frm.doc.contact_date) {
|
||||
let d = moment(this.frm.doc.contact_date);
|
||||
d.add(1, "day");
|
||||
this.frm.set_value("ends_on", d.format(frappe.defaultDatetimeFormat));
|
||||
}
|
||||
show_notes() {
|
||||
if (this.frm.doc.docstatus == 1) return;
|
||||
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: this.frm,
|
||||
notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
}
|
||||
|
||||
render_contact_day_html() {
|
||||
if (cur_frm.doc.contact_date) {
|
||||
let contact_date = frappe.datetime.obj_to_str(cur_frm.doc.contact_date);
|
||||
let diff_days = frappe.datetime.get_day_diff(contact_date, frappe.datetime.get_today());
|
||||
let color = diff_days > 0 ? "orange" : "green";
|
||||
let message = diff_days > 0 ? __("Next Contact Date") : __("Last Contact Date");
|
||||
let html = `<div class="col-xs-12">
|
||||
<span class="indicator whitespace-nowrap ${color}"><span> ${message} : ${frappe.datetime.global_date_format(contact_date)}</span></span>
|
||||
</div>` ;
|
||||
cur_frm.dashboard.set_headline_alert(html);
|
||||
}
|
||||
show_activities() {
|
||||
if (this.frm.doc.docstatus == 1) return;
|
||||
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: this.frm,
|
||||
open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(this.frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.LeadController({ frm: cur_frm }));
|
||||
|
||||
frappe.ui.form.on("Lead", {
|
||||
make_opportunity: async function(frm) {
|
||||
let existing_prospect = (await frappe.db.get_value("Prospect Lead",
|
||||
{
|
||||
"lead": frm.doc.name
|
||||
},
|
||||
"name", null, "Prospect"
|
||||
)).message.name;
|
||||
|
||||
if (!existing_prospect) {
|
||||
var fields = [
|
||||
{
|
||||
"label": "Create Prospect",
|
||||
"fieldname": "create_prospect",
|
||||
"fieldtype": "Check",
|
||||
"default": 1
|
||||
},
|
||||
{
|
||||
"label": "Prospect Name",
|
||||
"fieldname": "prospect_name",
|
||||
"fieldtype": "Data",
|
||||
"default": frm.doc.company_name,
|
||||
"depends_on": "create_prospect"
|
||||
}
|
||||
];
|
||||
}
|
||||
let existing_contact = (await frappe.db.get_value("Contact",
|
||||
{
|
||||
"first_name": frm.doc.first_name || frm.doc.lead_name,
|
||||
"last_name": frm.doc.last_name
|
||||
},
|
||||
"name"
|
||||
)).message.name;
|
||||
|
||||
if (!existing_contact) {
|
||||
fields.push(
|
||||
{
|
||||
"label": "Create Contact",
|
||||
"fieldname": "create_contact",
|
||||
"fieldtype": "Check",
|
||||
"default": "1"
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
if (fields) {
|
||||
var d = new frappe.ui.Dialog({
|
||||
title: __('Create Opportunity'),
|
||||
fields: fields,
|
||||
primary_action: function() {
|
||||
var data = d.get_values();
|
||||
frappe.call({
|
||||
method: 'create_prospect_and_contact',
|
||||
doc: frm.doc,
|
||||
args: {
|
||||
data: data,
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
if (!r.exc) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Create')
|
||||
});
|
||||
d.show();
|
||||
} else {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
@ -3,78 +3,80 @@
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"autoname": "naming_series:",
|
||||
"creation": "2013-04-10 11:45:37",
|
||||
"creation": "2022-02-08 13:14:41.083327",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Document",
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"lead_details",
|
||||
"naming_series",
|
||||
"salutation",
|
||||
"first_name",
|
||||
"middle_name",
|
||||
"last_name",
|
||||
"column_break_1",
|
||||
"lead_name",
|
||||
"col_break123",
|
||||
"status",
|
||||
"company_name",
|
||||
"designation",
|
||||
"job_title",
|
||||
"gender",
|
||||
"contact_details_section",
|
||||
"source",
|
||||
"col_break123",
|
||||
"lead_owner",
|
||||
"status",
|
||||
"customer",
|
||||
"type",
|
||||
"request_type",
|
||||
"contact_info_tab",
|
||||
"email_id",
|
||||
"website",
|
||||
"column_break_20",
|
||||
"mobile_no",
|
||||
"whatsapp_no",
|
||||
"column_break_16",
|
||||
"phone",
|
||||
"phone_ext",
|
||||
"additional_information_section",
|
||||
"organization_section",
|
||||
"company_name",
|
||||
"no_of_employees",
|
||||
"column_break_28",
|
||||
"annual_revenue",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"column_break_22",
|
||||
"column_break_31",
|
||||
"territory",
|
||||
"fax",
|
||||
"website",
|
||||
"type",
|
||||
"request_type",
|
||||
"address_section",
|
||||
"address_html",
|
||||
"column_break_38",
|
||||
"city",
|
||||
"pincode",
|
||||
"county",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"state",
|
||||
"country",
|
||||
"section_break_12",
|
||||
"lead_owner",
|
||||
"ends_on",
|
||||
"column_break_14",
|
||||
"contact_by",
|
||||
"contact_date",
|
||||
"lead_source_details_section",
|
||||
"company",
|
||||
"territory",
|
||||
"language",
|
||||
"column_break_50",
|
||||
"source",
|
||||
"column_break2",
|
||||
"contact_html",
|
||||
"qualification_tab",
|
||||
"qualification_status",
|
||||
"column_break_64",
|
||||
"qualified_by",
|
||||
"qualified_on",
|
||||
"other_info_tab",
|
||||
"campaign_name",
|
||||
"company",
|
||||
"column_break_22",
|
||||
"language",
|
||||
"image",
|
||||
"title",
|
||||
"column_break_50",
|
||||
"disabled",
|
||||
"unsubscribed",
|
||||
"blog_subscriber",
|
||||
"notes_section",
|
||||
"notes",
|
||||
"other_information_section",
|
||||
"customer",
|
||||
"image",
|
||||
"title"
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_tab",
|
||||
"notes_html",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "lead_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Details",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -86,6 +88,7 @@
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"in_global_search": 1,
|
||||
@ -108,7 +111,7 @@
|
||||
{
|
||||
"fieldname": "email_id",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email Address",
|
||||
"label": "Email",
|
||||
"oldfieldname": "email_id",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Email",
|
||||
@ -189,50 +192,9 @@
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldname": "contact_info_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Next Contact By",
|
||||
"oldfieldname": "contact_by",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "contact_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Next Contact Date",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "contact_date",
|
||||
"oldfieldtype": "Date",
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "ends_on",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Ends On",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "notes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Notes"
|
||||
"label": "Contact Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
@ -240,34 +202,6 @@
|
||||
"label": "Address HTML",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City/Town",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type"
|
||||
},
|
||||
{
|
||||
"fieldname": "county",
|
||||
"fieldtype": "Data",
|
||||
"label": "County"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"mandatory_depends_on": "eval: doc.address_title && doc.address_type",
|
||||
"options": "Country"
|
||||
},
|
||||
{
|
||||
"fieldname": "pincode",
|
||||
"fieldtype": "Data",
|
||||
"label": "Postal Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break"
|
||||
@ -289,7 +223,7 @@
|
||||
{
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Mobile No.",
|
||||
"label": "Mobile No",
|
||||
"oldfieldname": "mobile_no",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "Phone"
|
||||
@ -347,8 +281,7 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Website",
|
||||
"oldfieldname": "website",
|
||||
"oldfieldtype": "Data",
|
||||
"options": "URL"
|
||||
"oldfieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "territory",
|
||||
@ -380,14 +313,6 @@
|
||||
"label": "Title",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "designation",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Designation",
|
||||
"options": "Designation"
|
||||
},
|
||||
{
|
||||
"fieldname": "language",
|
||||
"fieldtype": "Link",
|
||||
@ -410,12 +335,6 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Last Name"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "additional_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
@ -428,35 +347,13 @@
|
||||
{
|
||||
"fieldname": "whatsapp_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "WhatsApp No.",
|
||||
"label": "WhatsApp",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "lead_source_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lead Source Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_50",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "other_information_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Other Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
@ -465,17 +362,156 @@
|
||||
"fieldname": "phone_ext",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Ext."
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "qualification_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Qualification"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "other_info_tab",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Information"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_1",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualified_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Qualified By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualified_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Qualified on"
|
||||
},
|
||||
{
|
||||
"fieldname": "qualification_status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Qualification Status",
|
||||
"options": "Unqualified\nIn Process\nQualified"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contacts"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_64",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_20",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_title",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Job Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_28",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "disabled",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disabled"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_38",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-user",
|
||||
"idx": 5,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-08-04 00:24:57.208590",
|
||||
"modified": "2022-06-27 21:56:17.392756",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Lead",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -535,6 +571,7 @@
|
||||
"show_name_in_global_search": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"subject_field": "title",
|
||||
"title_field": "title"
|
||||
}
|
@ -1,27 +1,19 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.utils import (
|
||||
comma_and,
|
||||
cstr,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
has_gravatar,
|
||||
nowdate,
|
||||
validate_email_address,
|
||||
)
|
||||
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
|
||||
|
||||
from erpnext.accounts.party import set_taxes
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
|
||||
|
||||
class Lead(SellingController):
|
||||
class Lead(SellingController, CRMNote):
|
||||
def get_feed(self):
|
||||
return "{0}: {1}".format(_(self.status), self.lead_name)
|
||||
|
||||
@ -29,6 +21,7 @@ class Lead(SellingController):
|
||||
customer = frappe.db.get_value("Customer", {"lead_name": self.name})
|
||||
self.get("__onload").is_customer = customer
|
||||
load_address_and_contact(self)
|
||||
self.set_onload("linked_prospects", self.get_linked_prospects())
|
||||
|
||||
def validate(self):
|
||||
self.set_full_name()
|
||||
@ -37,79 +30,42 @@ class Lead(SellingController):
|
||||
self.set_status()
|
||||
self.check_email_id_is_unique()
|
||||
self.validate_email_id()
|
||||
self.validate_contact_date()
|
||||
self.set_prev()
|
||||
|
||||
def before_insert(self):
|
||||
self.contact_doc = None
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
self.contact_doc = self.create_contact()
|
||||
|
||||
def after_insert(self):
|
||||
self.link_to_contact()
|
||||
|
||||
def on_update(self):
|
||||
self.update_prospect()
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
self.remove_link_from_prospect()
|
||||
|
||||
def set_full_name(self):
|
||||
if self.first_name:
|
||||
self.lead_name = " ".join(filter(None, [self.first_name, self.middle_name, self.last_name]))
|
||||
|
||||
def validate_email_id(self):
|
||||
if self.email_id:
|
||||
if not self.flags.ignore_email_validation:
|
||||
validate_email_address(self.email_id, throw=True)
|
||||
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead"))
|
||||
|
||||
if self.email_id == self.contact_by:
|
||||
frappe.throw(_("Next Contact By cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def validate_contact_date(self):
|
||||
if self.contact_date and getdate(self.contact_date) < getdate(nowdate()):
|
||||
frappe.throw(_("Next Contact Date cannot be in the past"))
|
||||
|
||||
if self.ends_on and self.contact_date and (getdate(self.ends_on) < getdate(self.contact_date)):
|
||||
frappe.throw(_("Ends On date cannot be before Next Contact Date."))
|
||||
|
||||
def on_update(self):
|
||||
self.add_calendar_event()
|
||||
self.update_prospects()
|
||||
|
||||
def set_prev(self):
|
||||
if self.is_new():
|
||||
self._prev = frappe._dict({"contact_date": None, "ends_on": None, "contact_by": None})
|
||||
else:
|
||||
self._prev = frappe.db.get_value(
|
||||
"Lead", self.name, ["contact_date", "ends_on", "contact_by"], as_dict=1
|
||||
self.lead_name = " ".join(
|
||||
filter(None, [self.salutation, self.first_name, self.middle_name, self.last_name])
|
||||
)
|
||||
|
||||
def before_insert(self):
|
||||
self.contact_doc = self.create_contact()
|
||||
def set_lead_name(self):
|
||||
if not self.lead_name:
|
||||
# Check for leads being created through data import
|
||||
if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
|
||||
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
|
||||
elif self.company_name:
|
||||
self.lead_name = self.company_name
|
||||
else:
|
||||
self.lead_name = self.email_id.split("@")[0]
|
||||
|
||||
def after_insert(self):
|
||||
self.update_links()
|
||||
|
||||
def update_links(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
self.contact_doc.append(
|
||||
"links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
|
||||
)
|
||||
self.contact_doc.save()
|
||||
|
||||
def add_calendar_event(self, opts=None, force=False):
|
||||
if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date"):
|
||||
super(Lead, self).add_calendar_event(
|
||||
{
|
||||
"owner": self.lead_owner,
|
||||
"starts_on": self.contact_date,
|
||||
"ends_on": self.ends_on or "",
|
||||
"subject": ("Contact " + cstr(self.lead_name)),
|
||||
"description": ("Contact " + cstr(self.lead_name))
|
||||
+ (self.contact_by and (". By : " + cstr(self.contact_by)) or ""),
|
||||
},
|
||||
force,
|
||||
)
|
||||
|
||||
def update_prospects(self):
|
||||
prospects = frappe.get_all("Prospect Lead", filters={"lead": self.name}, fields=["parent"])
|
||||
for row in prospects:
|
||||
prospect = frappe.get_doc("Prospect", row.parent)
|
||||
prospect.save(ignore_permissions=True)
|
||||
def set_title(self):
|
||||
self.title = self.company_name or self.lead_name
|
||||
|
||||
def check_email_id_is_unique(self):
|
||||
if self.email_id:
|
||||
@ -124,15 +80,47 @@ class Lead(SellingController):
|
||||
|
||||
if duplicate_leads:
|
||||
frappe.throw(
|
||||
_("Email Address must be unique, already exists for {0}").format(comma_and(duplicate_leads)),
|
||||
_("Email Address must be unique, it is already used in {0}").format(
|
||||
comma_and(duplicate_leads)
|
||||
),
|
||||
frappe.DuplicateEntryError,
|
||||
)
|
||||
|
||||
def on_trash(self):
|
||||
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name)
|
||||
def validate_email_id(self):
|
||||
if self.email_id:
|
||||
if not self.flags.ignore_email_validation:
|
||||
validate_email_address(self.email_id, throw=True)
|
||||
|
||||
self.unlink_dynamic_links()
|
||||
self.delete_events()
|
||||
if self.email_id == self.lead_owner:
|
||||
frappe.throw(_("Lead Owner cannot be same as the Lead Email Address"))
|
||||
|
||||
if self.is_new() or not self.image:
|
||||
self.image = has_gravatar(self.email_id)
|
||||
|
||||
def link_to_contact(self):
|
||||
# update contact links
|
||||
if self.contact_doc:
|
||||
self.contact_doc.append(
|
||||
"links", {"link_doctype": "Lead", "link_name": self.name, "link_title": self.lead_name}
|
||||
)
|
||||
self.contact_doc.save()
|
||||
|
||||
def update_prospect(self):
|
||||
lead_row_name = frappe.db.get_value(
|
||||
"Prospect Lead", filters={"lead": self.name}, fieldname="name"
|
||||
)
|
||||
if lead_row_name:
|
||||
lead_row = frappe.get_doc("Prospect Lead", lead_row_name)
|
||||
lead_row.update(
|
||||
{
|
||||
"lead_name": self.lead_name,
|
||||
"email": self.email_id,
|
||||
"mobile_no": self.mobile_no,
|
||||
"lead_owner": self.lead_owner,
|
||||
"status": self.status,
|
||||
}
|
||||
)
|
||||
lead_row.db_update()
|
||||
|
||||
def unlink_dynamic_links(self):
|
||||
links = frappe.get_all(
|
||||
@ -155,6 +143,30 @@ class Lead(SellingController):
|
||||
linked_doc.remove(to_remove)
|
||||
linked_doc.save(ignore_permissions=True)
|
||||
|
||||
def remove_link_from_prospect(self):
|
||||
prospects = self.get_linked_prospects()
|
||||
|
||||
for d in prospects:
|
||||
prospect = frappe.get_doc("Prospect", d.parent)
|
||||
if len(prospect.get("leads")) == 1:
|
||||
prospect.delete(ignore_permissions=True)
|
||||
else:
|
||||
to_remove = None
|
||||
for d in prospect.get("leads"):
|
||||
if d.lead == self.name:
|
||||
to_remove = d
|
||||
|
||||
if to_remove:
|
||||
prospect.remove(to_remove)
|
||||
prospect.save(ignore_permissions=True)
|
||||
|
||||
def get_linked_prospects(self):
|
||||
return frappe.get_all(
|
||||
"Prospect Lead",
|
||||
filters={"lead": self.name},
|
||||
fields=["parent"],
|
||||
)
|
||||
|
||||
def has_customer(self):
|
||||
return frappe.db.get_value("Customer", {"lead_name": self.name})
|
||||
|
||||
@ -171,50 +183,78 @@ class Lead(SellingController):
|
||||
"Quotation", {"party_name": self.name, "docstatus": 1, "status": "Lost"}
|
||||
)
|
||||
|
||||
def set_lead_name(self):
|
||||
if not self.lead_name:
|
||||
# Check for leads being created through data import
|
||||
if not self.company_name and not self.email_id and not self.flags.ignore_mandatory:
|
||||
frappe.throw(_("A Lead requires either a person's name or an organization's name"))
|
||||
elif self.company_name:
|
||||
self.lead_name = self.company_name
|
||||
else:
|
||||
self.lead_name = self.email_id.split("@")[0]
|
||||
@frappe.whitelist()
|
||||
def create_prospect_and_contact(self, data):
|
||||
data = frappe._dict(data)
|
||||
if data.create_contact:
|
||||
self.create_contact()
|
||||
|
||||
def set_title(self):
|
||||
self.title = self.company_name or self.lead_name
|
||||
if data.create_prospect:
|
||||
self.create_prospect(data.prospect_name)
|
||||
|
||||
def create_contact(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "auto_creation_of_contact"):
|
||||
if not self.lead_name:
|
||||
self.set_full_name()
|
||||
self.set_lead_name()
|
||||
if not self.lead_name:
|
||||
self.set_full_name()
|
||||
self.set_lead_name()
|
||||
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update(
|
||||
contact = frappe.new_doc("Contact")
|
||||
contact.update(
|
||||
{
|
||||
"first_name": self.first_name or self.lead_name,
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"job_title": self.job_title,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
)
|
||||
|
||||
if self.email_id:
|
||||
contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
|
||||
|
||||
if self.phone:
|
||||
contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
|
||||
|
||||
if self.mobile_no:
|
||||
contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.reload() # load changes by hooks on contact
|
||||
|
||||
return contact
|
||||
|
||||
def create_prospect(self, company_name):
|
||||
try:
|
||||
prospect = frappe.new_doc("Prospect")
|
||||
|
||||
prospect.company_name = company_name or self.company_name
|
||||
prospect.no_of_employees = self.no_of_employees
|
||||
prospect.industry = self.industry
|
||||
prospect.market_segment = self.market_segment
|
||||
prospect.annual_revenue = self.annual_revenue
|
||||
prospect.territory = self.territory
|
||||
prospect.fax = self.fax
|
||||
prospect.website = self.website
|
||||
prospect.prospect_owner = self.lead_owner
|
||||
prospect.company = self.company
|
||||
prospect.notes = self.notes
|
||||
|
||||
prospect.append(
|
||||
"leads",
|
||||
{
|
||||
"first_name": self.first_name or self.lead_name,
|
||||
"last_name": self.last_name,
|
||||
"salutation": self.salutation,
|
||||
"gender": self.gender,
|
||||
"designation": self.designation,
|
||||
"company_name": self.company_name,
|
||||
}
|
||||
"lead": self.name,
|
||||
"lead_name": self.lead_name,
|
||||
"email": self.email_id,
|
||||
"mobile_no": self.mobile_no,
|
||||
"lead_owner": self.lead_owner,
|
||||
"status": self.status,
|
||||
},
|
||||
)
|
||||
|
||||
if self.email_id:
|
||||
contact.append("email_ids", {"email_id": self.email_id, "is_primary": 1})
|
||||
|
||||
if self.phone:
|
||||
contact.append("phone_nos", {"phone": self.phone, "is_primary_phone": 1})
|
||||
|
||||
if self.mobile_no:
|
||||
contact.append("phone_nos", {"phone": self.mobile_no, "is_primary_mobile_no": 1})
|
||||
|
||||
contact.insert(ignore_permissions=True)
|
||||
contact.reload() # load changes by hooks on contact
|
||||
|
||||
return contact
|
||||
prospect.flags.ignore_permissions = True
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
except frappe.DuplicateEntryError:
|
||||
frappe.throw(_("Prospect {0} already exists").format(company_name or self.company_name))
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
@ -274,6 +314,8 @@ def make_opportunity(source_name, target_doc=None):
|
||||
"company_name": "customer_name",
|
||||
"email_id": "contact_email",
|
||||
"mobile_no": "contact_mobile",
|
||||
"lead_owner": "opportunity_owner",
|
||||
"notes": "notes",
|
||||
},
|
||||
}
|
||||
},
|
||||
@ -422,21 +464,25 @@ def get_lead_with_phone_number(number):
|
||||
return lead
|
||||
|
||||
|
||||
def daily_open_lead():
|
||||
leads = frappe.get_all("Lead", filters=[["contact_date", "Between", [nowdate(), nowdate()]]])
|
||||
for lead in leads:
|
||||
frappe.db.set_value("Lead", lead.name, "status", "Open")
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def add_lead_to_prospect(lead, prospect):
|
||||
prospect = frappe.get_doc("Prospect", prospect)
|
||||
prospect.append("prospect_lead", {"lead": lead})
|
||||
prospect.append("leads", {"lead": lead})
|
||||
prospect.save(ignore_permissions=True)
|
||||
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
)
|
||||
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Lead", lead, prospect)
|
||||
link_communications("Lead", lead, prospect)
|
||||
link_open_events("Lead", lead, prospect)
|
||||
|
||||
frappe.msgprint(
|
||||
_("Lead {0} has been added to prospect {1}.").format(
|
||||
frappe.bold(lead), frappe.bold(prospect.name)
|
||||
),
|
||||
title=_("Lead Added"),
|
||||
title=_("Lead -> Prospect"),
|
||||
indicator="green",
|
||||
)
|
||||
|
@ -16,7 +16,7 @@ frappe.listview_settings['Lead'] = {
|
||||
prospect.prospect_owner = r.lead_owner;
|
||||
|
||||
leads.forEach(function(lead) {
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
|
||||
let lead_prospect_row = frappe.model.add_child(prospect, 'leads');
|
||||
lead_prospect_row.lead = lead.name;
|
||||
});
|
||||
frappe.set_route("Form", "Prospect", prospect.name);
|
||||
|
@ -5,7 +5,10 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils import random_string, today
|
||||
|
||||
from erpnext.crm.doctype.lead.lead import make_opportunity
|
||||
from erpnext.crm.utils import get_linked_prospect
|
||||
|
||||
test_records = frappe.get_test_records("Lead")
|
||||
|
||||
@ -83,6 +86,105 @@ class TestLead(unittest.TestCase):
|
||||
self.assertEqual(frappe.db.exists("Lead", lead_doc.name), None)
|
||||
self.assertEqual(len(address_1.get("links")), 1)
|
||||
|
||||
def test_prospect_creation_from_lead(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
event = create_event("Meeting 1", today(), "Lead", lead.name)
|
||||
|
||||
lead.create_prospect(lead.company_name)
|
||||
|
||||
prospect = get_linked_prospect("Lead", lead.name)
|
||||
self.assertEqual(prospect, "Prospect Company")
|
||||
|
||||
event.reload()
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, prospect)
|
||||
|
||||
def test_opportunity_from_lead(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabOpportunity` where party_name='Rahul Tripathi'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
lead.add_note("test note")
|
||||
event = create_event("Meeting 1", today(), "Lead", lead.name)
|
||||
create_todo("followup", "Lead", lead.name)
|
||||
|
||||
opportunity = make_opportunity(lead.name)
|
||||
opportunity.save()
|
||||
|
||||
self.assertEqual(opportunity.get("party_name"), lead.name)
|
||||
self.assertEqual(opportunity.notes[0].note, "test note")
|
||||
|
||||
event.reload()
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Opportunity")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, opportunity.name)
|
||||
|
||||
self.assertTrue(
|
||||
frappe.db.get_value(
|
||||
"ToDo", {"reference_type": "Opportunity", "reference_name": opportunity.name}
|
||||
)
|
||||
)
|
||||
|
||||
def test_copy_events_from_lead_to_prospect(self):
|
||||
frappe.db.sql("delete from `tabLead` where lead_name='Rahul Tripathi'")
|
||||
frappe.db.sql("delete from `tabProspect` where name='Prospect Company'")
|
||||
|
||||
lead = make_lead(
|
||||
first_name="Rahul",
|
||||
last_name="Tripathi",
|
||||
email_id="rahul@gmail.com",
|
||||
company_name="Prospect Company",
|
||||
)
|
||||
|
||||
lead.create_prospect(lead.company_name)
|
||||
prospect = get_linked_prospect("Lead", lead.name)
|
||||
|
||||
event = create_event("Meeting", today(), "Lead", lead.name)
|
||||
|
||||
self.assertEqual(len(event.event_participants), 2)
|
||||
self.assertEqual(event.event_participants[1].reference_doctype, "Prospect")
|
||||
self.assertEqual(event.event_participants[1].reference_docname, prospect)
|
||||
|
||||
|
||||
def create_event(subject, starts_on, reference_type, reference_name):
|
||||
event = frappe.new_doc("Event")
|
||||
event.subject = subject
|
||||
event.starts_on = starts_on
|
||||
event.event_type = "Private"
|
||||
event.all_day = 1
|
||||
event.owner = "Administrator"
|
||||
event.append(
|
||||
"event_participants", {"reference_doctype": reference_type, "reference_docname": reference_name}
|
||||
)
|
||||
event.reference_type = reference_type
|
||||
event.reference_name = reference_name
|
||||
event.insert()
|
||||
return event
|
||||
|
||||
|
||||
def create_todo(description, reference_type, reference_name):
|
||||
todo = frappe.new_doc("ToDo")
|
||||
todo.description = description
|
||||
todo.owner = "Administrator"
|
||||
todo.reference_type = reference_type
|
||||
todo.reference_name = reference_name
|
||||
todo.insert()
|
||||
return todo
|
||||
|
||||
|
||||
def make_lead(**args):
|
||||
args = frappe._dict(args)
|
||||
@ -93,6 +195,7 @@ def make_lead(**args):
|
||||
"first_name": args.first_name or "_Test",
|
||||
"last_name": args.last_name or "Lead",
|
||||
"email_id": args.email_id or "new_lead_{}@example.com".format(random_string(5)),
|
||||
"company_name": args.company_name or "_Test Company",
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
@ -32,13 +32,6 @@ frappe.ui.form.on("Opportunity", {
|
||||
}
|
||||
},
|
||||
|
||||
contact_date: function(frm) {
|
||||
if(frm.doc.contact_date < frappe.datetime.now_datetime()){
|
||||
frm.set_value("contact_date", "");
|
||||
frappe.throw(__("Next follow up date should be greater than now."))
|
||||
}
|
||||
},
|
||||
|
||||
onload_post_render: function(frm) {
|
||||
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
|
||||
},
|
||||
@ -130,6 +123,13 @@ frappe.ui.form.on("Opportunity", {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (!frm.is_new()) {
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
// frm.trigger('render_contact_day_html');
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
},
|
||||
|
||||
set_contact_link: function(frm) {
|
||||
@ -227,8 +227,7 @@ frappe.ui.form.on("Opportunity", {
|
||||
'total': flt(total),
|
||||
'base_total': flt(base_total)
|
||||
});
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
frappe.ui.form.on("Opportunity Item", {
|
||||
calculate: function(frm, cdt, cdn) {
|
||||
@ -264,13 +263,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
this.frm.trigger('currency');
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.show_notes();
|
||||
this.show_activities();
|
||||
}
|
||||
|
||||
setup_queries() {
|
||||
var me = this;
|
||||
|
||||
if(this.frm.fields_dict.contact_by.df.options.match(/^User/)) {
|
||||
this.frm.set_query("contact_by", erpnext.queries.user);
|
||||
}
|
||||
|
||||
me.frm.set_query('customer_address', erpnext.queries.address_query);
|
||||
|
||||
this.frm.set_query("item_code", "items", function() {
|
||||
@ -287,6 +287,14 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
}
|
||||
else if (me.frm.doc.opportunity_from == "Customer") {
|
||||
me.frm.set_query('party_name', erpnext.queries['customer']);
|
||||
} else if (me.frm.doc.opportunity_from == "Prospect") {
|
||||
me.frm.set_query('party_name', function() {
|
||||
return {
|
||||
filters: {
|
||||
"company": me.frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -303,6 +311,24 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
|
||||
frm: cur_frm
|
||||
})
|
||||
}
|
||||
|
||||
show_notes() {
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: this.frm,
|
||||
notes_wrapper: $(this.frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
}
|
||||
|
||||
show_activities() {
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: this.frm,
|
||||
open_activities_wrapper: $(this.frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(this.frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(this.frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
};
|
||||
|
||||
extend_cscript(cur_frm.cscript, new erpnext.crm.Opportunity({frm: cur_frm}));
|
||||
|
@ -1,5 +1,6 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "naming_series:",
|
||||
@ -11,68 +12,87 @@
|
||||
"email_append_to": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_section",
|
||||
"naming_series",
|
||||
"opportunity_from",
|
||||
"party_name",
|
||||
"customer_name",
|
||||
"source",
|
||||
"column_break0",
|
||||
"title",
|
||||
"opportunity_type",
|
||||
"status",
|
||||
"converted_by",
|
||||
"column_break0",
|
||||
"opportunity_type",
|
||||
"source",
|
||||
"opportunity_owner",
|
||||
"column_break_10",
|
||||
"sales_stage",
|
||||
"first_response_time",
|
||||
"expected_closing",
|
||||
"next_contact",
|
||||
"contact_by",
|
||||
"contact_date",
|
||||
"column_break2",
|
||||
"to_discuss",
|
||||
"probability",
|
||||
"organization_details_section",
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"customer_group",
|
||||
"column_break_23",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"website",
|
||||
"column_break_31",
|
||||
"city",
|
||||
"state",
|
||||
"country",
|
||||
"territory",
|
||||
"section_break_14",
|
||||
"currency",
|
||||
"column_break_36",
|
||||
"conversion_rate",
|
||||
"base_opportunity_amount",
|
||||
"with_items",
|
||||
"column_break_17",
|
||||
"probability",
|
||||
"opportunity_amount",
|
||||
"base_opportunity_amount",
|
||||
"more_info",
|
||||
"company",
|
||||
"campaign",
|
||||
"transaction_date",
|
||||
"column_break1",
|
||||
"language",
|
||||
"amended_from",
|
||||
"title",
|
||||
"first_response_time",
|
||||
"lost_detail_section",
|
||||
"lost_reasons",
|
||||
"order_lost_reason",
|
||||
"column_break_56",
|
||||
"competitors",
|
||||
"contact_info",
|
||||
"primary_contact_section",
|
||||
"contact_person",
|
||||
"job_title",
|
||||
"column_break_54",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"column_break_22",
|
||||
"whatsapp",
|
||||
"phone",
|
||||
"phone_ext",
|
||||
"address_contact_section",
|
||||
"address_html",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break3",
|
||||
"contact_html",
|
||||
"contact_display",
|
||||
"items_section",
|
||||
"items",
|
||||
"section_break_32",
|
||||
"base_total",
|
||||
"column_break_33",
|
||||
"total",
|
||||
"contact_info",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"territory",
|
||||
"customer_group",
|
||||
"column_break3",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"more_info",
|
||||
"company",
|
||||
"campaign",
|
||||
"column_break1",
|
||||
"transaction_date",
|
||||
"language",
|
||||
"amended_from",
|
||||
"lost_detail_section",
|
||||
"lost_reasons",
|
||||
"order_lost_reason",
|
||||
"column_break_56",
|
||||
"competitors"
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_tab",
|
||||
"notes_html",
|
||||
"notes",
|
||||
"dashboard_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "from_section",
|
||||
"fieldtype": "Section Break",
|
||||
"options": "fa fa-user"
|
||||
},
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
@ -113,8 +133,9 @@
|
||||
"bold": 1,
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"in_global_search": 1,
|
||||
"label": "Customer / Lead Name",
|
||||
"label": "Customer Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -166,48 +187,10 @@
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Closing Date"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "contact_by",
|
||||
"fieldname": "next_contact",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Follow Up"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_by",
|
||||
"fieldtype": "Link",
|
||||
"in_standard_filter": 1,
|
||||
"label": "Next Contact By",
|
||||
"oldfieldname": "contact_by",
|
||||
"oldfieldtype": "Link",
|
||||
"options": "User",
|
||||
"width": "75px"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_date",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "Next Contact Date",
|
||||
"oldfieldname": "contact_date",
|
||||
"oldfieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break2",
|
||||
"fieldtype": "Column Break",
|
||||
"oldfieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_discuss",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "To Discuss",
|
||||
"no_copy": 1,
|
||||
"oldfieldname": "to_discuss",
|
||||
"oldfieldtype": "Small Text"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_14",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Sales"
|
||||
"label": "Opportunity Value"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
@ -221,12 +204,6 @@
|
||||
"label": "Opportunity Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "with_items",
|
||||
"fieldtype": "Check",
|
||||
"label": "With Items"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
@ -245,9 +222,8 @@
|
||||
"label": "Probability (%)"
|
||||
},
|
||||
{
|
||||
"depends_on": "with_items",
|
||||
"fieldname": "items_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Items",
|
||||
"oldfieldtype": "Section Break",
|
||||
"options": "fa fa-shopping-cart"
|
||||
@ -262,18 +238,16 @@
|
||||
"options": "Opportunity Item"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "next_contact_by",
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_info",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Contact Info",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Contacts",
|
||||
"options": "fa fa-bullhorn"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "customer_address",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 1,
|
||||
"label": "Customer / Lead Address",
|
||||
"options": "Address",
|
||||
"print_hide": 1
|
||||
@ -327,19 +301,16 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Email",
|
||||
"options": "Email",
|
||||
"read_only": 1
|
||||
"options": "Email"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.party_name",
|
||||
"fieldname": "contact_mobile",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Contact Mobile No",
|
||||
"read_only": 1
|
||||
"fieldtype": "Data",
|
||||
"label": "Contact Mobile",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
@ -416,12 +387,6 @@
|
||||
"options": "Opportunity Lost Reason Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "converted_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Converted By",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"fieldname": "first_response_time",
|
||||
@ -474,6 +439,7 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status===\"Lost\"",
|
||||
"fieldname": "lost_detail_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Lost Reasons"
|
||||
@ -488,12 +454,179 @@
|
||||
"label": "Competitors",
|
||||
"options": "Competitor Detail",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "organization_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Organization"
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
"label": "No of Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Annual Revenue"
|
||||
},
|
||||
{
|
||||
"fieldname": "industry",
|
||||
"fieldtype": "Link",
|
||||
"label": "Industry",
|
||||
"options": "Industry Type"
|
||||
},
|
||||
{
|
||||
"fieldname": "market_segment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Market Segment",
|
||||
"options": "Market Segment"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunity_owner",
|
||||
"fieldtype": "Link",
|
||||
"label": "Opportunity Owner",
|
||||
"options": "User"
|
||||
},
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_22",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "whatsapp",
|
||||
"fieldtype": "Data",
|
||||
"label": "WhatsApp",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone",
|
||||
"options": "Phone"
|
||||
},
|
||||
{
|
||||
"fieldname": "phone_ext",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Ext."
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_31",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "primary_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Primary Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_54",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "dashboard_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Dashboard",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "job_title",
|
||||
"fieldtype": "Data",
|
||||
"label": "Job Title"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Contact HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "city",
|
||||
"fieldtype": "Data",
|
||||
"label": "City"
|
||||
},
|
||||
{
|
||||
"fieldname": "state",
|
||||
"fieldtype": "Data",
|
||||
"label": "State"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Link",
|
||||
"label": "Country",
|
||||
"options": "Country"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-info-sign",
|
||||
"idx": 195,
|
||||
"links": [],
|
||||
"modified": "2022-01-29 19:32:26.382896",
|
||||
"modified": "2022-06-27 18:44:32.858696",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Opportunity",
|
||||
|
@ -6,53 +6,54 @@ import json
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.email.inbox import link_communication_to_document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
from frappe.query_builder import DocType, Interval
|
||||
from frappe.query_builder.functions import Now
|
||||
from frappe.utils import cint, flt, get_fullname
|
||||
from frappe.utils import flt, get_fullname
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.crm.utils import (
|
||||
CRMNote,
|
||||
copy_comments,
|
||||
link_communications,
|
||||
link_open_events,
|
||||
link_open_tasks,
|
||||
)
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
from erpnext.utilities.transaction_base import TransactionBase
|
||||
|
||||
|
||||
class Opportunity(TransactionBase):
|
||||
class Opportunity(TransactionBase, CRMNote):
|
||||
def onload(self):
|
||||
ref_doc = frappe.get_doc(self.opportunity_from, self.party_name)
|
||||
load_address_and_contact(ref_doc)
|
||||
self.set("__onload", ref_doc.get("__onload"))
|
||||
|
||||
def after_insert(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.get_doc("Lead", self.party_name).set_status(update=True)
|
||||
self.disable_lead()
|
||||
|
||||
if self.opportunity_from in ["Lead", "Prospect"]:
|
||||
link_open_tasks(self.opportunity_from, self.party_name, self)
|
||||
link_open_events(self.opportunity_from, self.party_name, self)
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
copy_comments(self.opportunity_from, self.party_name, self)
|
||||
add_link_in_communication(self.opportunity_from, self.party_name, self)
|
||||
link_communications(self.opportunity_from, self.party_name, self)
|
||||
|
||||
def validate(self):
|
||||
self._prev = frappe._dict(
|
||||
{
|
||||
"contact_date": frappe.db.get_value("Opportunity", self.name, "contact_date")
|
||||
if (not cint(self.get("__islocal")))
|
||||
else None,
|
||||
"contact_by": frappe.db.get_value("Opportunity", self.name, "contact_by")
|
||||
if (not cint(self.get("__islocal")))
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
self.make_new_lead_if_required()
|
||||
self.validate_item_details()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_cust_name()
|
||||
self.map_fields()
|
||||
self.set_exchange_rate()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.customer_name
|
||||
|
||||
if not self.with_items:
|
||||
self.items = []
|
||||
|
||||
else:
|
||||
self.calculate_totals()
|
||||
self.calculate_totals()
|
||||
self.update_prospect()
|
||||
|
||||
def map_fields(self):
|
||||
for field in self.meta.get_valid_columns():
|
||||
@ -63,18 +64,65 @@ class Opportunity(TransactionBase):
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def set_exchange_rate(self):
|
||||
company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
|
||||
if self.currency == company_currency:
|
||||
self.conversion_rate = 1.0
|
||||
return
|
||||
|
||||
if not self.conversion_rate or self.conversion_rate == 1.0:
|
||||
self.conversion_rate = get_exchange_rate(self.currency, company_currency, self.transaction_date)
|
||||
|
||||
def calculate_totals(self):
|
||||
total = base_total = 0
|
||||
for item in self.get("items"):
|
||||
item.amount = flt(item.rate) * flt(item.qty)
|
||||
item.base_rate = flt(self.conversion_rate * item.rate)
|
||||
item.base_amount = flt(self.conversion_rate * item.amount)
|
||||
item.base_rate = flt(self.conversion_rate) * flt(item.rate)
|
||||
item.base_amount = flt(self.conversion_rate) * flt(item.amount)
|
||||
total += item.amount
|
||||
base_total += item.base_amount
|
||||
|
||||
self.total = flt(total)
|
||||
self.base_total = flt(base_total)
|
||||
|
||||
def update_prospect(self):
|
||||
prospect_name = None
|
||||
if self.opportunity_from == "Prospect" and self.party_name:
|
||||
prospect_name = self.party_name
|
||||
elif self.opportunity_from == "Lead":
|
||||
prospect_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
|
||||
|
||||
if prospect_name:
|
||||
prospect = frappe.get_doc("Prospect", prospect_name)
|
||||
|
||||
opportunity_values = {
|
||||
"opportunity": self.name,
|
||||
"amount": self.opportunity_amount,
|
||||
"stage": self.sales_stage,
|
||||
"deal_owner": self.opportunity_owner,
|
||||
"probability": self.probability,
|
||||
"expected_closing": self.expected_closing,
|
||||
"currency": self.currency,
|
||||
"contact_person": self.contact_person,
|
||||
}
|
||||
|
||||
opportunity_already_added = False
|
||||
for d in prospect.get("opportunities", []):
|
||||
if d.opportunity == self.name:
|
||||
opportunity_already_added = True
|
||||
d.update(opportunity_values)
|
||||
d.db_update()
|
||||
|
||||
if not opportunity_already_added:
|
||||
prospect.append("opportunities", opportunity_values)
|
||||
prospect.flags.ignore_permissions = True
|
||||
prospect.flags.ignore_mandatory = True
|
||||
prospect.save()
|
||||
|
||||
def disable_lead(self):
|
||||
if self.opportunity_from == "Lead":
|
||||
frappe.db.set_value("Lead", self.party_name, {"disabled": 1, "docstatus": 1})
|
||||
|
||||
def make_new_lead_if_required(self):
|
||||
"""Set lead against new opportunity"""
|
||||
if (not self.get("party_name")) and self.contact_email:
|
||||
@ -144,11 +192,8 @@ class Opportunity(TransactionBase):
|
||||
else:
|
||||
frappe.throw(_("Cannot declare as lost, because Quotation has been made."))
|
||||
|
||||
def on_trash(self):
|
||||
self.delete_events()
|
||||
|
||||
def has_active_quotation(self):
|
||||
if not self.with_items:
|
||||
if not self.get("items", []):
|
||||
return frappe.get_all(
|
||||
"Quotation",
|
||||
{"opportunity": self.name, "status": ("not in", ["Lost", "Closed"]), "docstatus": 1},
|
||||
@ -165,7 +210,7 @@ class Opportunity(TransactionBase):
|
||||
)
|
||||
|
||||
def has_ordered_quotation(self):
|
||||
if not self.with_items:
|
||||
if not self.get("items", []):
|
||||
return frappe.get_all(
|
||||
"Quotation", {"opportunity": self.name, "status": "Ordered", "docstatus": 1}, "name"
|
||||
)
|
||||
@ -195,43 +240,20 @@ class Opportunity(TransactionBase):
|
||||
return True
|
||||
|
||||
def validate_cust_name(self):
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
lead_name, company_name = frappe.db.get_value(
|
||||
"Lead", self.party_name, ["lead_name", "company_name"]
|
||||
)
|
||||
self.customer_name = company_name or lead_name
|
||||
if self.party_name:
|
||||
if self.opportunity_from == "Customer":
|
||||
self.customer_name = frappe.db.get_value("Customer", self.party_name, "customer_name")
|
||||
elif self.opportunity_from == "Lead":
|
||||
customer_name = frappe.db.get_value("Prospect Lead", {"lead": self.party_name}, "parent")
|
||||
if not customer_name:
|
||||
lead_name, company_name = frappe.db.get_value(
|
||||
"Lead", self.party_name, ["lead_name", "company_name"]
|
||||
)
|
||||
customer_name = company_name or lead_name
|
||||
|
||||
def on_update(self):
|
||||
self.add_calendar_event()
|
||||
|
||||
def add_calendar_event(self, opts=None, force=False):
|
||||
if frappe.db.get_single_value("CRM Settings", "create_event_on_next_contact_date_opportunity"):
|
||||
if not opts:
|
||||
opts = frappe._dict()
|
||||
|
||||
opts.description = ""
|
||||
opts.contact_date = self.contact_date
|
||||
|
||||
if self.party_name and self.opportunity_from == "Customer":
|
||||
if self.contact_person:
|
||||
opts.description = f"Contact {self.contact_person}"
|
||||
else:
|
||||
opts.description = f"Contact customer {self.party_name}"
|
||||
elif self.party_name and self.opportunity_from == "Lead":
|
||||
if self.contact_display:
|
||||
opts.description = f"Contact {self.contact_display}"
|
||||
else:
|
||||
opts.description = f"Contact lead {self.party_name}"
|
||||
|
||||
opts.subject = opts.description
|
||||
opts.description += f". By : {self.contact_by}"
|
||||
|
||||
if self.to_discuss:
|
||||
opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}"
|
||||
|
||||
super(Opportunity, self).add_calendar_event(opts, force)
|
||||
self.customer_name = customer_name
|
||||
elif self.opportunity_from == "Prospect":
|
||||
self.customer_name = self.party_name
|
||||
|
||||
def validate_item_details(self):
|
||||
if not self.get("items"):
|
||||
@ -295,7 +317,7 @@ def make_quotation(source_name, target_doc=None):
|
||||
|
||||
quotation.run_method("set_missing_values")
|
||||
quotation.run_method("calculate_taxes_and_totals")
|
||||
if not source.with_items:
|
||||
if not source.get("items", []):
|
||||
quotation.opportunity = source.name
|
||||
|
||||
doclist = get_mapped_doc(
|
||||
@ -440,34 +462,3 @@ def make_opportunity_from_communication(communication, company, ignore_communica
|
||||
link_communication_to_document(doc, "Opportunity", opportunity.name, ignore_communication_links)
|
||||
|
||||
return opportunity.name
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_events(start, end, filters=None):
|
||||
"""Returns events for Gantt / Calendar view rendering.
|
||||
:param start: Start date-time.
|
||||
:param end: End date-time.
|
||||
:param filters: Filters (JSON).
|
||||
"""
|
||||
from frappe.desk.calendar import get_event_conditions
|
||||
|
||||
conditions = get_event_conditions("Opportunity", filters)
|
||||
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
distinct `tabOpportunity`.name, `tabOpportunity`.customer_name, `tabOpportunity`.opportunity_amount,
|
||||
`tabOpportunity`.title, `tabOpportunity`.contact_date
|
||||
from
|
||||
`tabOpportunity`
|
||||
where
|
||||
(`tabOpportunity`.contact_date between %(start)s and %(end)s)
|
||||
{conditions}
|
||||
""".format(
|
||||
conditions=conditions
|
||||
),
|
||||
{"start": start, "end": end},
|
||||
as_dict=True,
|
||||
update={"allDay": 0},
|
||||
)
|
||||
return data
|
||||
|
@ -1,19 +0,0 @@
|
||||
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
frappe.views.calendar["Opportunity"] = {
|
||||
field_map: {
|
||||
"start": "contact_date",
|
||||
"end": "contact_date",
|
||||
"id": "name",
|
||||
"title": "customer_name",
|
||||
"allDay": "allDay"
|
||||
},
|
||||
options: {
|
||||
header: {
|
||||
left: 'prev,next today',
|
||||
center: 'title',
|
||||
right: 'month'
|
||||
}
|
||||
},
|
||||
get_events_method: 'erpnext.crm.doctype.opportunity.opportunity.get_events'
|
||||
}
|
@ -77,42 +77,6 @@ class TestOpportunity(unittest.TestCase):
|
||||
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
|
||||
create_communication(opp_doc.doctype, opp_doc.name, opp_doc.contact_email)
|
||||
|
||||
quotation_doc = make_quotation(opp_doc.name)
|
||||
quotation_doc.append("items", {"item_code": "_Test Item", "qty": 1})
|
||||
quotation_doc.run_method("set_missing_values")
|
||||
quotation_doc.run_method("calculate_taxes_and_totals")
|
||||
quotation_doc.save()
|
||||
|
||||
quotation_comment_count = frappe.db.count(
|
||||
"Comment",
|
||||
{
|
||||
"reference_doctype": quotation_doc.doctype,
|
||||
"reference_name": quotation_doc.name,
|
||||
"comment_type": "Comment",
|
||||
},
|
||||
)
|
||||
quotation_communication_count = len(
|
||||
get_linked_communication_list(quotation_doc.doctype, quotation_doc.name)
|
||||
)
|
||||
self.assertEqual(quotation_comment_count, 4)
|
||||
self.assertEqual(quotation_communication_count, 4)
|
||||
|
||||
def test_render_template_for_to_discuss(self):
|
||||
doc = make_opportunity(with_items=0, opportunity_from="Lead")
|
||||
doc.contact_by = "test@example.com"
|
||||
doc.contact_date = add_days(today(), days=2)
|
||||
doc.to_discuss = "{{ doc.name }} test data"
|
||||
doc.save()
|
||||
|
||||
event = frappe.get_all(
|
||||
"Event Participants",
|
||||
fields=["parent"],
|
||||
filters={"reference_doctype": doc.doctype, "reference_docname": doc.name},
|
||||
)
|
||||
|
||||
event_description = frappe.db.get_value("Event", event[0].parent, "description")
|
||||
self.assertTrue(doc.name in event_description)
|
||||
|
||||
|
||||
def make_opportunity_from_lead():
|
||||
new_lead_email_id = "new{}@example.com".format(random_string(5))
|
||||
@ -139,7 +103,6 @@ def make_opportunity(**args):
|
||||
"opportunity_from": args.opportunity_from or "Customer",
|
||||
"opportunity_type": "Sales",
|
||||
"conversion_rate": 1.0,
|
||||
"with_items": args.with_items or 0,
|
||||
"transaction_date": today(),
|
||||
}
|
||||
)
|
||||
|
@ -8,7 +8,9 @@
|
||||
"transaction_date": "2013-12-12",
|
||||
"items": [{
|
||||
"item_name": "Test Item",
|
||||
"description": "Some description"
|
||||
"description": "Some description",
|
||||
"qty": 5,
|
||||
"rate": 100
|
||||
}]
|
||||
}
|
||||
]
|
||||
|
@ -27,5 +27,26 @@ frappe.ui.form.on('Prospect', {
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
frm.trigger("show_notes");
|
||||
frm.trigger("show_activities");
|
||||
},
|
||||
|
||||
show_notes (frm) {
|
||||
const crm_notes = new erpnext.utils.CRMNotes({
|
||||
frm: frm,
|
||||
notes_wrapper: $(frm.fields_dict.notes_html.wrapper),
|
||||
});
|
||||
crm_notes.refresh();
|
||||
},
|
||||
|
||||
show_activities (frm) {
|
||||
const crm_activities = new erpnext.utils.CRMActivities({
|
||||
frm: frm,
|
||||
open_activities_wrapper: $(frm.fields_dict.open_activities_html.wrapper),
|
||||
all_activities_wrapper: $(frm.fields_dict.all_activities_html.wrapper),
|
||||
form_wrapper: $(frm.wrapper),
|
||||
});
|
||||
crm_activities.refresh();
|
||||
}
|
||||
|
||||
});
|
||||
|
@ -1,33 +1,42 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_events_in_timeline": 1,
|
||||
"autoname": "field:company_name",
|
||||
"creation": "2021-08-19 00:21:06.995448",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"overview_tab",
|
||||
"company_name",
|
||||
"industry",
|
||||
"market_segment",
|
||||
"customer_group",
|
||||
"no_of_employees",
|
||||
"annual_revenue",
|
||||
"column_break_4",
|
||||
"market_segment",
|
||||
"industry",
|
||||
"territory",
|
||||
"column_break_6",
|
||||
"no_of_employees",
|
||||
"currency",
|
||||
"annual_revenue",
|
||||
"more_details_section",
|
||||
"fax",
|
||||
"website",
|
||||
"column_break_13",
|
||||
"prospect_owner",
|
||||
"website",
|
||||
"fax",
|
||||
"company",
|
||||
"leads_section",
|
||||
"prospect_lead",
|
||||
"address_and_contact_section",
|
||||
"column_break_16",
|
||||
"contacts_tab",
|
||||
"address_html",
|
||||
"column_break_17",
|
||||
"column_break_18",
|
||||
"contact_html",
|
||||
"leads_section",
|
||||
"leads",
|
||||
"opportunities_tab",
|
||||
"opportunities",
|
||||
"activities_tab",
|
||||
"open_activities_html",
|
||||
"all_activities_section",
|
||||
"all_activities_html",
|
||||
"notes_section",
|
||||
"notes_html",
|
||||
"notes"
|
||||
],
|
||||
"fields": [
|
||||
@ -71,15 +80,9 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "no_of_employees",
|
||||
"fieldtype": "Int",
|
||||
"label": "No. of Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
"fieldtype": "Select",
|
||||
"label": "No. of Employees",
|
||||
"options": "1-10\n11-20\n21-30\n31-100\n11-50\n51-200\n201-500\n101-500\n500-1000\n501-1000\n>1000\n1000+"
|
||||
},
|
||||
{
|
||||
"fieldname": "annual_revenue",
|
||||
@ -97,8 +100,7 @@
|
||||
{
|
||||
"fieldname": "website",
|
||||
"fieldtype": "Data",
|
||||
"label": "Website",
|
||||
"options": "URL"
|
||||
"label": "Website"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_owner",
|
||||
@ -108,23 +110,14 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "leads_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Leads"
|
||||
},
|
||||
{
|
||||
"fieldname": "prospect_lead",
|
||||
"fieldtype": "Table",
|
||||
"options": "Prospect Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Address HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_17",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "contact_html",
|
||||
"fieldtype": "HTML",
|
||||
@ -132,28 +125,16 @@
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "notes_section",
|
||||
"fieldtype": "Section Break",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Notes"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Text Editor"
|
||||
},
|
||||
{
|
||||
"fieldname": "more_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "More Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.__islocal",
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"label": "Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
@ -161,11 +142,83 @@
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Opportunities"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "activities_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Notes HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "opportunities",
|
||||
"fieldtype": "Table",
|
||||
"label": "Opportunities",
|
||||
"options": "Prospect Opportunity"
|
||||
},
|
||||
{
|
||||
"fieldname": "contacts_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_18",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "leads",
|
||||
"fieldtype": "Table",
|
||||
"options": "Prospect Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_16",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "overview_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Overview"
|
||||
},
|
||||
{
|
||||
"fieldname": "open_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Open Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "All Activities"
|
||||
},
|
||||
{
|
||||
"fieldname": "all_activities_html",
|
||||
"fieldtype": "HTML",
|
||||
"label": "All Activities HTML"
|
||||
},
|
||||
{
|
||||
"fieldname": "notes",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 1,
|
||||
"label": "Notes",
|
||||
"no_copy": 1,
|
||||
"options": "CRM Note"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-11-01 13:10:36.759249",
|
||||
"modified": "2022-06-21 15:10:26.887502",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect",
|
||||
@ -207,6 +260,7 @@
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"title_field": "company_name",
|
||||
"track_changes": 1
|
||||
}
|
@ -3,19 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import load_address_and_contact
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.crm.utils import add_link_in_communication, copy_comments
|
||||
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
|
||||
|
||||
|
||||
class Prospect(Document):
|
||||
class Prospect(CRMNote):
|
||||
def onload(self):
|
||||
load_address_and_contact(self)
|
||||
|
||||
def validate(self):
|
||||
self.update_lead_details()
|
||||
|
||||
def on_update(self):
|
||||
self.link_with_lead_contact_and_address()
|
||||
|
||||
@ -23,23 +19,24 @@ class Prospect(Document):
|
||||
self.unlink_dynamic_links()
|
||||
|
||||
def after_insert(self):
|
||||
if frappe.db.get_single_value("CRM Settings", "carry_forward_communication_and_comments"):
|
||||
for row in self.get("prospect_lead"):
|
||||
copy_comments("Lead", row.lead, self)
|
||||
add_link_in_communication("Lead", row.lead, self)
|
||||
carry_forward_communication_and_comments = frappe.db.get_single_value(
|
||||
"CRM Settings", "carry_forward_communication_and_comments"
|
||||
)
|
||||
|
||||
def update_lead_details(self):
|
||||
for row in self.get("prospect_lead"):
|
||||
lead = frappe.get_value(
|
||||
"Lead", row.lead, ["lead_name", "status", "email_id", "mobile_no"], as_dict=True
|
||||
)
|
||||
row.lead_name = lead.lead_name
|
||||
row.status = lead.status
|
||||
row.email = lead.email_id
|
||||
row.mobile_no = lead.mobile_no
|
||||
for row in self.get("leads"):
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Lead", row.lead, self)
|
||||
link_communications("Lead", row.lead, self)
|
||||
link_open_events("Lead", row.lead, self)
|
||||
|
||||
for row in self.get("opportunities"):
|
||||
if carry_forward_communication_and_comments:
|
||||
copy_comments("Opportunity", row.opportunity, self)
|
||||
link_communications("Opportunity", row.opportunity, self)
|
||||
link_open_events("Opportunity", row.opportunity, self)
|
||||
|
||||
def link_with_lead_contact_and_address(self):
|
||||
for row in self.prospect_lead:
|
||||
for row in self.leads:
|
||||
links = frappe.get_all(
|
||||
"Dynamic Link",
|
||||
filters={"link_doctype": "Lead", "link_name": row.lead},
|
||||
@ -116,9 +113,7 @@ def make_opportunity(source_name, target_doc=None):
|
||||
{
|
||||
"Prospect": {
|
||||
"doctype": "Opportunity",
|
||||
"field_map": {
|
||||
"name": "party_name",
|
||||
},
|
||||
"field_map": {"name": "party_name", "prospect_owner": "opportunity_owner"},
|
||||
}
|
||||
},
|
||||
target_doc,
|
||||
@ -127,3 +122,25 @@ def make_opportunity(source_name, target_doc=None):
|
||||
)
|
||||
|
||||
return doclist
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_opportunities(prospect):
|
||||
return frappe.get_all(
|
||||
"Opportunity",
|
||||
filters={"opportunity_from": "Prospect", "party_name": prospect},
|
||||
fields=[
|
||||
"opportunity_owner",
|
||||
"sales_stage",
|
||||
"status",
|
||||
"expected_closing",
|
||||
"probability",
|
||||
"opportunity_amount",
|
||||
"currency",
|
||||
"contact_person",
|
||||
"contact_email",
|
||||
"contact_mobile",
|
||||
"creation",
|
||||
"name",
|
||||
],
|
||||
)
|
||||
|
@ -20,7 +20,7 @@ class TestProspect(unittest.TestCase):
|
||||
add_lead_to_prospect(lead_doc.name, prospect_doc.name)
|
||||
prospect_doc.reload()
|
||||
lead_exists_in_prosoect = False
|
||||
for rec in prospect_doc.get("prospect_lead"):
|
||||
for rec in prospect_doc.get("leads"):
|
||||
if rec.lead == lead_doc.name:
|
||||
lead_exists_in_prosoect = True
|
||||
self.assertEqual(lead_exists_in_prosoect, True)
|
||||
|
@ -7,12 +7,15 @@
|
||||
"field_order": [
|
||||
"lead",
|
||||
"lead_name",
|
||||
"status",
|
||||
"email",
|
||||
"mobile_no"
|
||||
"column_break_4",
|
||||
"mobile_no",
|
||||
"lead_owner",
|
||||
"status"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "lead",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
@ -21,6 +24,8 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.lead_name",
|
||||
"fieldname": "lead_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -28,14 +33,17 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "lead.status",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.email_id",
|
||||
"fieldname": "email",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
@ -44,18 +52,32 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "lead.mobile_no",
|
||||
"fieldname": "mobile_no",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Mobile No",
|
||||
"options": "Phone",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "lead.lead_owner",
|
||||
"fieldname": "lead_owner",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Lead Owner"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-25 12:58:24.638054",
|
||||
"modified": "2022-04-28 20:27:58.805970",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect Lead",
|
||||
@ -63,5 +85,6 @@
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": [],
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,101 @@
|
||||
{
|
||||
"actions": [],
|
||||
"autoname": "autoincrement",
|
||||
"creation": "2022-04-27 17:40:37.965161",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"opportunity",
|
||||
"amount",
|
||||
"stage",
|
||||
"deal_owner",
|
||||
"column_break_4",
|
||||
"probability",
|
||||
"expected_closing",
|
||||
"currency",
|
||||
"contact_person"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "opportunity",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Opportunity",
|
||||
"options": "Opportunity"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.opportunity_amount",
|
||||
"fieldname": "amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Amount",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.sales_stage",
|
||||
"fieldname": "stage",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Stage"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "opportunity.probability",
|
||||
"fieldname": "probability",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "Probability"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"fetch_from": "opportunity.expected_closing",
|
||||
"fieldname": "expected_closing",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "Closing"
|
||||
},
|
||||
{
|
||||
"fetch_from": "opportunity.currency",
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_4",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fetch_from": "opportunity.opportunity_owner",
|
||||
"fieldname": "deal_owner",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Deal Owner"
|
||||
},
|
||||
{
|
||||
"fetch_from": "opportunity.contact_person",
|
||||
"fieldname": "contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Contact Person",
|
||||
"options": "Contact"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-28 10:05:38.730368",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Prospect Opportunity",
|
||||
"naming_rule": "Autoincrement",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class ProspectOpportunity(Document):
|
||||
pass
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user