Merge branch 'develop' into lcv-future-stock-update

This commit is contained in:
Marica 2022-07-19 12:53:51 +05:30 committed by GitHub
commit 2a2db8c64a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
350 changed files with 11484 additions and 222333 deletions

View File

@ -66,6 +66,7 @@ ignore =
F841,
E713,
E712,
B023
max-line-length = 200

View File

@ -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

View File

@ -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

View File

@ -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

30
.github/workflows/semantic-commits.yml vendored Normal file
View File

@ -0,0 +1,30 @@
name: Semantic Commits
on:
pull_request: {}
permissions:
contents: read
concurrency:
group: commitcheck-erpnext-${{ github.event.number }}
cancel-in-progress: true
jobs:
commitlint:
name: Check Commit Titles
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 200
- uses: actions/setup-node@v3
with:
node-version: 14
check-latest: true
- name: Check commit titles
run: |
npm install @commitlint/cli @commitlint/config-conventional
npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}

View File

@ -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 }}-

View File

@ -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 }}-

View File

@ -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

View File

@ -1 +0,0 @@
hypothesis~=6.31.0

View File

@ -18,6 +18,7 @@
"automatically_fetch_payment_terms",
"column_break_17",
"enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account",
"report_setting_section",
"use_custom_cash_flow",
"deferred_accounting_settings_section",
@ -339,6 +340,13 @@
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
},
{
"default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account "
}
],
"icon": "icon-cog",
@ -346,7 +354,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-04-08 14:45:06.796418",
"modified": "2022-07-11 13:37:50.605141",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -1 +0,0 @@
C Form (India specific only) - Will be deprecated.

View File

@ -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);
});
}
}
});

View File

@ -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
}

View File

@ -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,
}

View File

@ -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

View File

@ -1 +0,0 @@
Invoice detail for parent C-Form.

View File

@ -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
}

View File

@ -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

View File

@ -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": [

View File

@ -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
}

View File

@ -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

View File

@ -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");
}
});
}
}
},

View File

@ -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",

View File

@ -1204,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):

View File

@ -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
}
};
});
}
});

View File

@ -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);
}
});
}
}
});

View File

@ -162,7 +162,7 @@ class PaymentReconciliation(Document):
{
"reference_type": inv.voucher_type,
"reference_name": inv.voucher_no,
"amount": -(inv.outstanding),
"amount": -(inv.outstanding_in_account_currency),
"posting_date": inv.posting_date,
"currency": inv.currency,
}

View File

@ -19,6 +19,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_company()
self.create_item()
self.create_customer()
self.create_account()
self.clear_old_entries()
def tearDown(self):
@ -89,6 +90,38 @@ class TestPaymentReconciliation(FrappeTestCase):
customer.save()
self.customer2 = customer.name
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 create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
@ -118,7 +151,7 @@ class TestPaymentReconciliation(FrappeTestCase):
)
return sinv
def create_payment_entry(self, amount=100, posting_date=nowdate()):
def create_payment_entry(self, amount=100, posting_date=nowdate(), customer=None):
"""
Helper function to populate default values in payment entry
"""
@ -126,7 +159,7 @@ class TestPaymentReconciliation(FrappeTestCase):
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
party=customer or self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=amount,
@ -454,3 +487,59 @@ class TestPaymentReconciliation(FrappeTestCase):
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")

View File

@ -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",

View File

@ -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'));
@ -572,9 +572,10 @@ frappe.ui.form.on("Purchase Invoice", {
},
is_subcontracted: function(frm) {
if (frm.doc.is_subcontracted) {
if (frm.doc.is_old_subcontracting_flow) {
erpnext.buying.get_default_bom(frm);
}
frm.toggle_reqd("supplier_warehouse", frm.doc.is_subcontracted);
},

View File

@ -169,7 +169,8 @@
"column_break_114",
"auto_repeat",
"update_auto_repeat_reference",
"per_received"
"per_received",
"is_old_subcontracting_flow"
],
"fields": [
{
@ -547,7 +548,8 @@
"fieldname": "is_subcontracted",
"fieldtype": "Check",
"label": "Is Subcontracted",
"print_hide": 1
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "items_section",
@ -1365,7 +1367,7 @@
"width": "50px"
},
{
"depends_on": "eval:doc.update_stock && doc.is_subcontracted",
"depends_on": "eval:doc.is_subcontracted",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
@ -1416,13 +1418,21 @@
"label": "Advance Tax",
"options": "Advance Tax",
"read_only": 1
},
{
"default": "0",
"fieldname": "is_old_subcontracting_flow",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Old Subcontracting Flow",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
"modified": "2021-11-25 13:31:02.716727",
"modified": "2022-06-15 15:40:58.527065",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",

View File

@ -502,7 +502,10 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
self.set_consumed_qty_in_po()
if self.is_old_subcontracting_flow:
self.set_consumed_qty_in_subcontract_order()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@ -1405,7 +1408,9 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
self.set_consumed_qty_in_po()
if self.is_old_subcontracting_flow:
self.set_consumed_qty_in_subcontract_order()
self.make_gl_entries_on_cancel()

View File

@ -1,3 +0,0 @@
{% include "erpnext/regional/india/taxes.js" %}
erpnext.setup_auto_gst_taxation('Purchase Invoice');

View File

@ -470,37 +470,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
self.assertEqual(tax.tax_amount, expected_values[i][1])
self.assertEqual(tax.total, expected_values[i][2])
def test_purchase_invoice_with_subcontracted_item(self):
wrapper = frappe.copy_doc(test_records[0])
wrapper.get("items")[0].item_code = "_Test FG Item"
wrapper.insert()
wrapper.load_from_db()
expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]]
for i, item in enumerate(wrapper.get("items")):
self.assertEqual(item.item_code, expected_values[i][0])
self.assertEqual(item.item_tax_amount, expected_values[i][1])
self.assertEqual(item.valuation_rate, expected_values[i][2])
self.assertEqual(wrapper.base_net_total, 1250)
# tax amounts
expected_values = [
["_Test Account Shipping Charges - _TC", 100, 1350],
["_Test Account Customs Duty - _TC", 125, 1350],
["_Test Account Excise Duty - _TC", 140, 1490],
["_Test Account Education Cess - _TC", 2.8, 1492.8],
["_Test Account S&H Education Cess - _TC", 1.4, 1494.2],
["_Test Account CST - _TC", 29.88, 1524.08],
["_Test Account VAT - _TC", 156.25, 1680.33],
["_Test Account Discount - _TC", 168.03, 1512.30],
]
for i, tax in enumerate(wrapper.get("taxes")):
self.assertEqual(tax.account_head, expected_values[i][0])
self.assertEqual(tax.tax_amount, expected_values[i][1])
self.assertEqual(tax.total, expected_values[i][2])
def test_purchase_invoice_with_advance(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import (
test_records as jv_test_records,
@ -961,30 +930,6 @@ class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
pi.cancel()
self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
update_backflush_based_on("BOM")
make_stock_entry(
item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
)
make_stock_entry(
item_code="_Test Item Home Desktop 100",
target="_Test Warehouse 1 - _TC",
qty=100,
basic_rate=100,
)
pi = make_purchase_invoice(
item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted=1
)
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
pi = make_purchase_invoice(
item_code="_Test Serialized Item With Series",
@ -1474,15 +1419,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 +1458,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 +1466,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 +1492,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(
@ -1654,40 +1614,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",
"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,
},
)
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()
}
],
}
).insert(ignore_if_duplicate=True)
def unlink_payment_on_cancel_of_invoice(enable=1):

View File

@ -619,10 +619,13 @@
"search_index": 1
},
{
"depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
"options": "BOM"
"options": "BOM",
"read_only": 1,
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
},
{
"default": "0",

View File

@ -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"));
}
}
});

View File

@ -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'
});
}
});
};

View File

@ -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();
},

View File

@ -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",

View File

@ -151,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:
@ -366,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:
@ -815,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:
@ -1030,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(
{
@ -1055,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")
@ -1512,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):
@ -2117,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)
@ -2181,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"]:
@ -2224,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():

View File

@ -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"]},
],
}

View File

@ -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()
@ -1797,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()
@ -1838,7 +1868,6 @@ class TestSalesInvoice(unittest.TestCase):
"items",
{
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": 100,
"rate": 50,
@ -1851,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,
@ -1938,7 +1966,6 @@ class TestSalesInvoice(unittest.TestCase):
"items",
{
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": 1,
"rate": rate,
@ -2611,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")
@ -3153,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()
@ -3165,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()
@ -3276,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
@ -3489,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",
@ -3542,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,

View File

@ -132,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:

View File

@ -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>

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -8,11 +8,11 @@ from frappe import _
def execute(filters=None):
validate_filters(filters)
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
tds_docs, tds_accounts, tax_category_map, journal_entry_party_map = get_tds_docs(filters)
columns = get_columns(filters)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map)
return columns, res
@ -22,10 +22,11 @@ def validate_filters(filters):
frappe.throw(_("From Date must be before To Date"))
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_result(filters, tds_docs, tds_accounts, tax_category_map, journal_entry_party_map):
supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(tds_docs)
print(journal_entry_party_map)
out = []
for name, details in gle_map.items():
@ -38,6 +39,11 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if voucher_type == "Journal Entry":
suppliers = journal_entry_party_map.get(name)
if suppliers:
supplier = suppliers[0]
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get("tax_withholding_category")
rate = tax_rate_map.get(tax_withholding_category)
@ -176,6 +182,7 @@ def get_tds_docs(filters):
journal_entries = []
tax_category_map = {}
or_filters = {}
journal_entry_party_map = {}
bank_accounts = frappe.get_all("Account", {"is_group": 0, "account_type": "Bank"}, pluck="name")
tds_accounts = frappe.get_all(
@ -218,9 +225,24 @@ def get_tds_docs(filters):
get_tax_category_map(payment_entries, "Payment Entry", tax_category_map)
if journal_entries:
journal_entry_party_map = get_journal_entry_party_map(journal_entries)
get_tax_category_map(journal_entries, "Journal Entry", tax_category_map)
return tds_documents, tds_accounts, tax_category_map
return tds_documents, tds_accounts, tax_category_map, journal_entry_party_map
def get_journal_entry_party_map(journal_entries):
journal_entry_party_map = {}
for d in frappe.db.get_all(
"Journal Entry Account",
{"parent": ("in", journal_entries), "party_type": "Supplier", "party": ("is", "set")},
["parent", "party"],
):
if d.parent not in journal_entry_party_map:
journal_entry_party_map[d.parent] = []
journal_entry_party_map[d.parent].append(d.party)
return journal_entry_party_map
def get_tax_category_map(vouchers, doctype, tax_category_map):

View File

@ -855,8 +855,8 @@ def get_outstanding_invoices(
)
for d in invoice_list:
payment_amount = d.invoice_amount - d.outstanding
outstanding_amount = d.outstanding
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 (
min_outstanding
@ -872,7 +872,7 @@ def get_outstanding_invoices(
"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,
@ -1412,7 +1412,7 @@ def create_payment_ledger_entry(
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,

View File

@ -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",

View File

@ -740,51 +740,6 @@ 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)
asset = create_asset(
calculate_depreciation=1,
available_for_use_date="2030-07-12",
purchase_date="2030-01-01",
finance_book=finance_book.name,
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,
)
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],
]
schedules = [
[
cstr(d.schedule_date),
flt(d.depreciation_amount, 2),
flt(d.accumulated_depreciation_amount, 2),
]
for d in asset.get("schedules")
]
self.assertEqual(schedules, expected_schedules)
# reset indian company
frappe.flags.company = company_flag
class TestDepreciationBasics(AssetSetup):
def test_depreciation_without_pro_rata(self):

View File

@ -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):
@ -132,9 +129,6 @@ class AssetValueAdjustment(Document):
days = date_diff(data.schedule_date, from_date)
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)

View File

@ -8,6 +8,7 @@ frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Purchase Order", {
setup: function(frm) {
if (frm.doc.is_old_subcontracting_flow) {
frm.set_query("reserve_warehouse", "supplied_items", function() {
return {
filters: {
@ -17,6 +18,7 @@ frappe.ui.form.on("Purchase Order", {
}
}
});
}
frm.set_indicator_formatter('item_code',
function(doc) { return (doc.qty<=doc.received_qty) ? "green" : "orange" })
@ -28,12 +30,67 @@ frappe.ui.form.on("Purchase Order", {
}
});
frm.set_query("fg_item", "items", function() {
return {
filters: {
'is_sub_contracted_item': 1,
'default_bom': ['!=', '']
}
}
});
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
refresh: function(frm) {
if(frm.doc.is_old_subcontracting_flow) {
frm.trigger('get_materials_from_supplier');
$('a.grey-link').each(function () {
var id = $(this).children(':first-child').attr('data-label');
if (id == 'Duplicate') {
$(this).remove();
return false;
}
});
}
},
get_materials_from_supplier: function(frm) {
let po_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
}
});
}
if (po_details && po_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.controllers.subcontracting_controller.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: {
subcontract_order: frm.doc.name,
rm_details: po_details,
order_doctype: cur_frm.doc.doctype
},
callback: function(r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
},
onload: function(frm) {
set_schedule_date(frm);
if (!frm.doc.transaction_date){
@ -52,39 +109,6 @@ frappe.ui.form.on("Purchase Order", {
frm.set_value("tax_withholding_category", frm.supplier_tds);
}
},
refresh: function(frm) {
frm.trigger('get_materials_from_supplier');
},
get_materials_from_supplier: function(frm) {
let po_details = [];
if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
frm.doc.supplied_items.forEach(d => {
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
po_details.push(d.name)
}
});
}
if (po_details && po_details.length) {
frm.add_custom_button(__('Return of Components'), () => {
frm.call({
method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
freeze: true,
freeze_message: __('Creating Stock Entry'),
args: { purchase_order: frm.doc.name, po_details: po_details },
callback: function(r) {
if (r && r.message) {
const doc = frappe.model.sync(r.message);
frappe.set_route("Form", doc[0].doctype, doc[0].name);
}
}
});
}, __('Create'));
}
}
});
frappe.ui.form.on("Purchase Order Item", {
@ -97,6 +121,16 @@ frappe.ui.form.on("Purchase Order Item", {
set_schedule_date(frm);
}
}
},
qty: function(frm, cdt, cdn) {
if (frm.doc.is_subcontracted && !frm.doc.is_old_subcontracting_flow) {
var row = locals[cdt][cdn];
if (row.qty) {
row.fg_item_qty = row.qty;
}
}
}
});
@ -105,12 +139,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
this.frm.custom_make_buttons = {
'Purchase Receipt': 'Purchase Receipt',
'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment',
'Subcontracting Order': 'Subcontracting Order',
'Stock Entry': 'Material to Supplier'
}
super.setup();
}
refresh(doc, cdt, cdn) {
@ -142,6 +176,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
if(!in_list(["Closed", "Delivered"], doc.status)) {
if(this.frm.doc.status !== 'Closed' && flt(this.frm.doc.per_received) < 100 && flt(this.frm.doc.per_billed) < 100) {
// Don't add Update Items button if the PO is following the new subcontracting flow.
if (!(this.frm.doc.is_subcontracted && !this.frm.doc.is_old_subcontracting_flow)) {
this.frm.add_custom_button(__('Update Items'), () => {
erpnext.utils.update_child_items({
frm: this.frm,
@ -151,6 +187,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
})
});
}
}
if (this.frm.has_perm("submit")) {
if(flt(doc.per_billed, 6) < 100 || flt(doc.per_received, 6) < 100) {
if (doc.status != "On Hold") {
@ -177,9 +214,15 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
if (doc.status != "On Hold") {
if(flt(doc.per_received) < 100 && allow_receipt) {
cur_frm.add_custom_button(__('Purchase Receipt'), this.make_purchase_receipt, __('Create'));
if(doc.is_subcontracted && me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'),
function() { me.make_stock_entry(); }, __("Transfer"));
if (doc.is_subcontracted) {
if (doc.is_old_subcontracting_flow) {
if (me.has_unsupplied_items()) {
cur_frm.add_custom_button(__('Material to Supplier'), function() { me.make_stock_entry(); }, __("Transfer"));
}
}
else {
cur_frm.add_custom_button(__('Subcontracting Order'), this.make_subcontracting_order, __('Create'));
}
}
}
if(flt(doc.per_billed) < 100)
@ -370,10 +413,11 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
_make_rm_stock_entry(rm_items) {
frappe.call({
method:"erpnext.buying.doctype.purchase_order.purchase_order.make_rm_stock_entry",
method:"erpnext.controllers.subcontracting_controller.make_rm_stock_entry",
args: {
purchase_order: cur_frm.doc.name,
rm_items: rm_items
subcontract_order: cur_frm.doc.name,
rm_items: rm_items,
order_doctype: cur_frm.doc.doctype
}
,
callback: function(r) {
@ -405,6 +449,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
})
}
make_subcontracting_order() {
frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_subcontracting_order",
frm: cur_frm,
freeze_message: __("Creating Subcontracting Order ...")
})
}
add_from_mappers() {
var me = this;
this.frm.add_custom_button(__('Material Request'),
@ -425,7 +477,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"));
@ -613,7 +665,8 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc,
}
}
cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) {
if (cur_frm.doc.is_old_subcontracting_flow) {
cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt, cdn) {
var d = locals[cdt][cdn]
return {
filters: [
@ -623,6 +676,7 @@ cur_frm.fields_dict['items'].grid.get_field('bom').get_query = function(doc, cdt
['BOM', 'company', '=', doc.company]
]
}
}
}
function set_schedule_date(frm) {
@ -634,7 +688,7 @@ function set_schedule_date(frm) {
frappe.provide("erpnext.buying");
frappe.ui.form.on("Purchase Order", "is_subcontracted", function(frm) {
if (frm.doc.is_subcontracted) {
if (frm.doc.is_old_subcontracting_flow) {
erpnext.buying.get_default_bom(frm);
}
});

View File

@ -16,6 +16,8 @@
"supplier_name",
"apply_tds",
"tax_withholding_category",
"is_subcontracted",
"supplier_warehouse",
"column_break1",
"company",
"transaction_date",
@ -55,10 +57,7 @@
"price_list_currency",
"plc_conversion_rate",
"ignore_pricing_rule",
"sec_warehouse",
"is_subcontracted",
"col_break_warehouse",
"supplier_warehouse",
"section_break_45",
"before_items_section",
"scan_barcode",
"items_col_break",
@ -142,7 +141,8 @@
"party_account_currency",
"is_internal_supplier",
"represents_company",
"inter_company_order_reference"
"inter_company_order_reference",
"is_old_subcontracting_flow"
],
"fields": [
{
@ -158,7 +158,8 @@
"hidden": 1,
"label": "Title",
"no_copy": 1,
"print_hide": 1
"print_hide": 1,
"reqd": 1
},
{
"fieldname": "naming_series",
@ -443,11 +444,6 @@
"permlevel": 1,
"print_hide": 1
},
{
"fieldname": "sec_warehouse",
"fieldtype": "Section Break",
"label": "Subcontracting"
},
{
"description": "Sets 'Warehouse' in each row of the Items table.",
"fieldname": "set_warehouse",
@ -456,15 +452,10 @@
"options": "Warehouse",
"print_hide": 1
},
{
"fieldname": "col_break_warehouse",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "is_subcontracted",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Is Subcontracted",
"print_hide": 1
},
@ -1142,6 +1133,10 @@
"label": "Tax Withholding Category",
"options": "Tax Withholding Category"
},
{
"fieldname": "section_break_45",
"fieldtype": "Section Break"
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
@ -1163,13 +1158,21 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"default": "0",
"fieldname": "is_old_subcontracting_flow",
"fieldtype": "Check",
"hidden": 1,
"label": "Is Old Subcontracting Flow",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2022-04-26 12:16:38.694276",
"modified": "2022-06-15 15:40:58.527065",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -69,8 +69,12 @@ class PurchaseOrder(BuyingController):
self.validate_with_previous_doc()
self.validate_for_subcontracting()
self.validate_minimum_order_qty()
if self.is_old_subcontracting_flow:
self.validate_bom_for_subcontracting_items()
self.create_raw_materials_supplied("supplied_items")
self.create_raw_materials_supplied()
self.validate_fg_item_for_subcontracting()
self.set_received_qty_for_drop_ship_items()
validate_inter_company_party(
self.doctype, self.supplier, self.company, self.inter_company_order_reference
@ -194,12 +198,38 @@ class PurchaseOrder(BuyingController):
)
def validate_bom_for_subcontracting_items(self):
if self.is_subcontracted:
for item in self.items:
if not item.bom:
frappe.throw(
_("BOM is not specified for subcontracting item {0} at row {1}").format(
item.item_code, item.idx
_("Row #{0}: BOM is not specified for subcontracting item {0}").format(
item.idx, item.item_code
)
)
def validate_fg_item_for_subcontracting(self):
if self.is_subcontracted and not self.is_old_subcontracting_flow:
for item in self.items:
if not item.fg_item:
frappe.throw(
_("Row #{0}: Finished Good Item is not specified for service item {1}").format(
item.idx, item.item_code
)
)
else:
if not frappe.get_value("Item", item.fg_item, "is_sub_contracted_item"):
frappe.throw(
_(
"Row #{0}: Finished Good Item {1} must be a sub-contracted item for service item {2}"
).format(item.idx, item.fg_item, item.item_code)
)
elif not frappe.get_value("Item", item.fg_item, "default_bom"):
frappe.throw(
_("Row #{0}: Default BOM not found for FG Item {1}").format(item.idx, item.fg_item)
)
if not item.fg_item_qty:
frappe.throw(
_("Row #{0}: Finished Good Item Qty is not specified for service item {0}").format(
item.idx, item.item_code
)
)
@ -294,9 +324,7 @@ class PurchaseOrder(BuyingController):
self.set_status(update=True, status=status)
self.update_requested_qty()
self.update_ordered_qty()
if self.is_subcontracted:
self.update_reserved_qty_for_subcontract()
self.notify_update()
clear_doctype_notifications(self)
@ -310,8 +338,6 @@ class PurchaseOrder(BuyingController):
self.update_requested_qty()
self.update_ordered_qty()
self.validate_budget()
if self.is_subcontracted:
self.update_reserved_qty_for_subcontract()
frappe.get_doc("Authorization Control").validate_approving_authority(
@ -332,9 +358,7 @@ class PurchaseOrder(BuyingController):
if self.has_drop_ship_item():
self.update_delivered_qty_in_sales_order()
if self.is_subcontracted:
self.update_reserved_qty_for_subcontract()
self.check_on_hold_or_closed_status()
frappe.db.set(self, "status", "Cancelled")
@ -405,10 +429,11 @@ class PurchaseOrder(BuyingController):
item.received_qty = item.qty
def update_reserved_qty_for_subcontract(self):
if self.is_old_subcontracting_flow:
for d in self.supplied_items:
if d.rm_item_code:
stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse)
stock_bin.update_reserved_qty_for_sub_contracting()
stock_bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order")
def update_receiving_percentage(self):
total_qty, received_qty = 0.0, 0.0
@ -587,80 +612,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
return doc
@frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items):
rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
fg_items = list(set(d["item_code"] for d in rm_items_list))
else:
frappe.throw(_("No Items selected for transfer"))
if purchase_order:
purchase_order = frappe.get_doc("Purchase Order", purchase_order)
if fg_items:
items = tuple(set(d["rm_item_code"] for d in rm_items_list))
item_wh = get_item_details(items)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = "Send to Subcontractor"
stock_entry.purchase_order = purchase_order.name
stock_entry.supplier = purchase_order.supplier
stock_entry.supplier_name = purchase_order.supplier_name
stock_entry.supplier_address = purchase_order.supplier_address
stock_entry.address_display = purchase_order.address_display
stock_entry.company = purchase_order.company
stock_entry.to_warehouse = purchase_order.supplier_warehouse
stock_entry.set_stock_entry_type()
for item_code in fg_items:
for rm_item_data in rm_items_list:
if rm_item_data["item_code"] == item_code:
rm_item_code = rm_item_data["rm_item_code"]
items_dict = {
rm_item_code: {
"po_detail": rm_item_data.get("name"),
"item_name": rm_item_data["item_name"],
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item_data["qty"],
"from_warehouse": rm_item_data["warehouse"],
"stock_uom": rm_item_data["stock_uom"],
"serial_no": rm_item_data.get("serial_no"),
"batch_no": rm_item_data.get("batch_no"),
"main_item_code": rm_item_data["item_code"],
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
stock_entry.set_missing_values()
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
return purchase_order.name
def get_item_details(items):
item_details = {}
for d in frappe.db.sql(
"""select item_code, description, allow_alternative_item from `tabItem`
where name in ({0})""".format(
", ".join(["%s"] * len(items))
),
items,
as_dict=1,
):
item_details[d.item_code] = d
return item_details
def get_list_context(context=None):
from erpnext.controllers.website_list_for_contact import get_list_context
@ -691,61 +642,61 @@ def make_inter_company_sales_order(source_name, target_doc=None):
@frappe.whitelist()
def get_materials_from_supplier(purchase_order, po_details):
if isinstance(po_details, str):
po_details = json.loads(po_details)
doc = frappe.get_cached_doc("Purchase Order", purchase_order)
doc.initialized_fields()
doc.purchase_orders = [doc.name]
doc.get_available_materials()
if not doc.available_materials:
frappe.throw(
_("Materials are already received against the purchase order {0}").format(purchase_order)
)
return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
def make_subcontracting_order(source_name, target_doc=None):
return get_mapped_subcontracting_order(source_name, target_doc)
def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
ste_doc = frappe.new_doc("Stock Entry")
ste_doc.purpose = "Material Transfer"
ste_doc.purchase_order = po_doc.name
ste_doc.company = po_doc.company
ste_doc.is_return = 1
def get_mapped_subcontracting_order(source_name, target_doc=None):
for key, value in available_materials.items():
if not value.qty:
continue
if target_doc and isinstance(target_doc, str):
target_doc = json.loads(target_doc)
for key in ["service_items", "items", "supplied_items"]:
if key in target_doc:
del target_doc[key]
target_doc = json.dumps(target_doc)
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, po_details)
ste_doc.set_stock_entry_type()
ste_doc.set_missing_values()
return ste_doc
def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
item = ste_doc.append("items", row.item_details)
po_detail = list(set(row.po_details).intersection(po_details))
item.update(
target_doc = get_mapped_doc(
"Purchase Order",
source_name,
{
"qty": qty,
"batch_no": batch_no,
"basic_rate": row.item_details["rate"],
"po_detail": po_detail[0] if po_detail else "",
"s_warehouse": row.item_details["t_warehouse"],
"t_warehouse": row.item_details["s_warehouse"],
"item_code": row.item_details["rm_item_code"],
"subcontracted_item": row.item_details["main_item_code"],
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
}
"Purchase Order": {
"doctype": "Subcontracting Order",
"field_map": {},
"field_no_map": ["total_qty", "total", "net_total"],
"validation": {
"docstatus": ["=", 1],
},
},
"Purchase Order Item": {
"doctype": "Subcontracting Order Service Item",
"field_map": {},
"field_no_map": [],
},
},
target_doc,
)
target_doc.populate_items_table()
if target_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = target_doc.set_warehouse
else:
source_doc = frappe.get_doc("Purchase Order", source_name)
if source_doc.set_warehouse:
for item in target_doc.items:
item.warehouse = source_doc.set_warehouse
else:
for idx, item in enumerate(target_doc.items):
item.warehouse = source_doc.items[idx].warehouse
return target_doc
@frappe.whitelist()
def is_subcontracting_order_created(po_name) -> bool:
count = frappe.db.count(
"Subcontracting Order", {"purchase_order": po_name, "status": ["not in", ["Draft", "Cancelled"]]}
)
return True if count else False

View File

@ -22,6 +22,6 @@ def get_data():
"label": _("Reference"),
"items": ["Material Request", "Supplier Quotation", "Project", "Auto Repeat"],
},
{"label": _("Sub-contracting"), "items": ["Stock Entry"]},
{"label": _("Sub-contracting"), "items": ["Subcontracting Order", "Stock Entry"]},
],
}

View File

@ -1,3 +0,0 @@
{% include "erpnext/regional/india/taxes.js" %}
erpnext.setup_auto_gst_taxation('Purchase Order');

View File

@ -13,9 +13,6 @@ from erpnext.buying.doctype.purchase_order.purchase_order import (
make_purchase_invoice as make_pi_from_po,
)
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.purchase_order import (
make_rm_stock_entry as make_subcontract_transfer_entry,
)
from erpnext.controllers.accounts_controller import update_child_qty_rate
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.item.test_item import make_item
@ -24,7 +21,6 @@ from erpnext.stock.doctype.material_request.test_material_request import make_ma
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import (
make_purchase_invoice as make_pi_from_pr,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestPurchaseOrder(FrappeTestCase):
@ -389,31 +385,6 @@ class TestPurchaseOrder(FrappeTestCase):
new_item_with_tax.delete()
frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
trans_item = json.dumps(
[
{
"item_code": po.get("items")[0].item_code,
"rate": po.get("items")[0].rate,
"qty": po.get("items")[0].qty,
"uom": "_Test UOM 1",
"conversion_factor": 2,
"docname": po.get("items")[0].name,
}
]
)
update_child_qty_rate("Purchase Order", trans_item, po.name)
po.reload()
total_reqd_qty_after_change = sum(
d.get("required_qty") for d in po.as_dict().get("supplied_items")
)
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
def test_update_qty(self):
po = create_purchase_order()
@ -572,10 +543,6 @@ class TestPurchaseOrder(FrappeTestCase):
)
automatically_fetch_payment_terms(enable=0)
def test_subcontracting(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
self.assertEqual(len(po.get("supplied_items")), 2)
def test_warehouse_company_validation(self):
from erpnext.stock.utils import InvalidWarehouseCompany
@ -740,379 +707,6 @@ class TestPurchaseOrder(FrappeTestCase):
pi.insert()
self.assertTrue(pi.get("payment_schedule"))
def test_reserved_qty_subcontract_po(self):
# Make stock available for raw materials
make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=30, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code="_Test Item Home Desktop 100",
qty=30,
basic_rate=100,
)
bin1 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
as_dict=1,
)
# Submit PO
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
bin2 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname=["reserved_qty_for_sub_contract", "projected_qty", "modified"],
as_dict=1,
)
self.assertEqual(bin2.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
self.assertEqual(bin2.projected_qty, bin1.projected_qty - 10)
self.assertNotEqual(bin1.modified, bin2.modified)
# Create stock transfer
rm_item = [
{
"item_code": "_Test FG Item",
"rm_item_code": "_Test Item",
"item_name": "_Test Item",
"qty": 6,
"warehouse": "_Test Warehouse - _TC",
"rate": 100,
"amount": 600,
"stock_uom": "Nos",
}
]
rm_item_string = json.dumps(rm_item)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.to_warehouse = "_Test Warehouse 1 - _TC"
se.save()
se.submit()
bin3 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin3.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
# close PO
po.update_status("Closed")
bin4 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin4.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
# Re-open PO
po.update_status("Submitted")
bin5 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin5.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
make_stock_entry(
target="_Test Warehouse 1 - _TC", item_code="_Test Item", qty=40, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse 1 - _TC",
item_code="_Test Item Home Desktop 100",
qty=40,
basic_rate=100,
)
# make Purchase Receipt against PO
pr = make_purchase_receipt(po.name)
pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
pr.save()
pr.submit()
bin6 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin6.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
# Cancel PR
pr.cancel()
bin7 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin7.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
# Make Purchase Invoice
pi = make_pi_from_po(po.name)
pi.update_stock = 1
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.insert()
pi.submit()
bin8 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin8.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
# Cancel PR
pi.cancel()
bin9 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin9.reserved_qty_for_sub_contract, bin2.reserved_qty_for_sub_contract - 6)
# Cancel Stock Entry
se.cancel()
bin10 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin10.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract + 10)
# Cancel PO
po.reload()
po.cancel()
bin11 = frappe.db.get_value(
"Bin",
filters={"warehouse": "_Test Warehouse - _TC", "item_code": "_Test Item"},
fieldname="reserved_qty_for_sub_contract",
as_dict=1,
)
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self):
item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)
po = create_purchase_order(
item_code=item_code,
qty=1,
is_subcontracted=1,
supplier_warehouse="_Test Warehouse 1 - _TC",
include_exploded_items=1,
)
name = frappe.db.get_value("BOM", {"item": item_code}, "name")
bom = frappe.get_doc("BOM", name)
exploded_items = sorted(
[d.item_code for d in bom.exploded_items if not d.get("sourced_by_supplier")]
)
supplied_items = sorted([d.rm_item_code for d in po.supplied_items])
self.assertEqual(exploded_items, supplied_items)
po1 = create_purchase_order(
item_code=item_code,
qty=1,
is_subcontracted=1,
supplier_warehouse="_Test Warehouse 1 - _TC",
include_exploded_items=0,
)
supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items])
bom_items = sorted([d.item_code for d in bom.items if not d.get("sourced_by_supplier")])
self.assertEqual(supplied_items1, bom_items)
def test_backflush_based_on_stock_entry(self):
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code=item_code)
make_item("Sub Contracted Raw Material 1", {"is_stock_item": 1, "is_sub_contracted_item": 1})
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 5
po = create_purchase_order(
item_code=item_code,
qty=order_qty,
is_subcontracted=1,
supplier_warehouse="_Test Warehouse 1 - _TC",
)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="Test Extra Item 1", qty=100, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse - _TC", item_code="Test Extra Item 2", qty=10, basic_rate=100
)
make_stock_entry(
target="_Test Warehouse - _TC",
item_code="Sub Contracted Raw Material 1",
qty=10,
basic_rate=100,
)
rm_items = [
{
"item_code": item_code,
"rm_item_code": "Sub Contracted Raw Material 1",
"item_name": "_Test Item",
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
{
"item_code": item_code,
"rm_item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
"qty": 20,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
{
"item_code": item_code,
"rm_item_code": "Test Extra Item 1",
"item_name": "Test Extra Item 1",
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
{
"item_code": item_code,
"rm_item_code": "Test Extra Item 2",
"stock_uom": "Nos",
"qty": 10,
"warehouse": "_Test Warehouse - _TC",
"item_name": "Test Extra Item 2",
},
]
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
pr = make_purchase_receipt(po.name)
received_qty = 2
# partial receipt
pr.get("items")[0].qty = received_qty
pr.save()
pr.submit()
transferred_items = sorted(
[d.item_code for d in se.get("items") if se.purchase_order == po.name]
)
issued_items = sorted([d.rm_item_code for d in pr.get("supplied_items")])
self.assertEqual(transferred_items, issued_items)
self.assertEqual(pr.get("items")[0].rm_supp_cost, 2000)
transferred_rm_map = frappe._dict()
for item in rm_items:
transferred_rm_map[item.get("rm_item_code")] = item
update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self):
item_code = "_Test Subcontracted FG Item 5"
make_item("Sub Contracted Raw Material 4", {"is_stock_item": 1, "is_sub_contracted_item": 1})
make_subcontracted_item(item_code=item_code, raw_materials=["Sub Contracted Raw Material 4"])
update_backflush_based_on("Material Transferred for Subcontract")
order_qty = 250
po = create_purchase_order(
item_code=item_code,
qty=order_qty,
is_subcontracted=1,
supplier_warehouse="_Test Warehouse 1 - _TC",
do_not_save=True,
)
# Add same subcontracted items multiple times
po.append(
"items",
{
"item_code": item_code,
"qty": order_qty,
"schedule_date": add_days(nowdate(), 1),
"warehouse": "_Test Warehouse - _TC",
},
)
po.set_missing_values()
po.submit()
# Material receipt entry for the raw materials which will be send to supplier
make_stock_entry(
target="_Test Warehouse - _TC",
item_code="Sub Contracted Raw Material 4",
qty=500,
basic_rate=100,
)
rm_items = [
{
"item_code": item_code,
"rm_item_code": "Sub Contracted Raw Material 4",
"item_name": "_Test Item",
"qty": 250,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
"name": po.supplied_items[0].name,
},
{
"item_code": item_code,
"rm_item_code": "Sub Contracted Raw Material 4",
"item_name": "_Test Item",
"qty": 250,
"warehouse": "_Test Warehouse - _TC",
"stock_uom": "Nos",
},
]
# Raw Materials transfer entry from stores to supplier's warehouse
rm_item_string = json.dumps(rm_items)
se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
se.submit()
# Test po_detail field has value or not
for item_row in se.items:
self.assertEqual(item_row.po_detail, po.supplied_items[item_row.idx - 1].name)
po_doc = frappe.get_doc("Purchase Order", po.name)
for row in po_doc.supplied_items:
# Valid that whether transferred quantity is matching with supplied qty or not in the purchase order
self.assertEqual(row.supplied_qty, 250.0)
update_backflush_based_on("BOM")
def test_advance_payment_entry_unlink_against_purchase_order(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
@ -1211,50 +805,6 @@ def make_pr_against_po(po, received_qty=0):
return pr
def make_subcontracted_item(**args):
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
args = frappe._dict(args)
if not frappe.db.exists("Item", args.item_code):
make_item(
args.item_code,
{
"is_stock_item": 1,
"is_sub_contracted_item": 1,
"has_batch_no": args.get("has_batch_no") or 0,
},
)
if not args.raw_materials:
if not frappe.db.exists("Item", "Test Extra Item 1"):
make_item(
"Test Extra Item 1",
{
"is_stock_item": 1,
},
)
if not frappe.db.exists("Item", "Test Extra Item 2"):
make_item(
"Test Extra Item 2",
{
"is_stock_item": 1,
},
)
args.raw_materials = ["_Test FG Item", "Test Extra Item 1"]
if not frappe.db.get_value("BOM", {"item": args.item_code}, "name"):
make_bom(item=args.item_code, raw_materials=args.get("raw_materials"))
def update_backflush_based_on(based_on):
doc = frappe.get_doc("Buying Settings")
doc.backflush_raw_materials_of_subcontract_based_on = based_on
doc.save()
def get_same_items():
return [
{

View File

@ -1,38 +1,4 @@
[
{
"advance_paid": 0.0,
"buying_price_list": "_Test Price List",
"company": "_Test Company",
"conversion_rate": 1.0,
"currency": "INR",
"doctype": "Purchase Order",
"base_grand_total": 5000.0,
"grand_total": 5000.0,
"is_subcontracted": 1,
"naming_series": "_T-Purchase Order-",
"base_net_total": 5000.0,
"items": [
{
"base_amount": 5000.0,
"conversion_factor": 1.0,
"description": "_Test FG Item",
"doctype": "Purchase Order Item",
"item_code": "_Test FG Item",
"item_name": "_Test FG Item",
"parentfield": "items",
"qty": 10.0,
"rate": 500.0,
"schedule_date": "2013-03-01",
"stock_uom": "_Test UOM",
"uom": "_Test UOM",
"warehouse": "_Test Warehouse - _TC"
}
],
"supplier": "_Test Supplier",
"supplier_name": "_Test Supplier",
"transaction_date": "2013-02-12",
"schedule_date": "2013-02-13"
},
{
"advance_paid": 0.0,
"buying_price_list": "_Test Price List",

View File

@ -11,6 +11,8 @@
"supplier_part_no",
"item_name",
"product_bundle",
"fg_item",
"fg_item_qty",
"column_break_4",
"schedule_date",
"expected_delivery_date",
@ -574,16 +576,18 @@
"read_only": 1
},
{
"depends_on": "eval:parent.is_subcontracted",
"depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "bom",
"fieldtype": "Link",
"label": "BOM",
"options": "BOM",
"print_hide": 1
"print_hide": 1,
"read_only": 1,
"read_only_depends_on": "eval:!parent.is_old_subcontracting_flow"
},
{
"default": "0",
"depends_on": "eval:parent.is_subcontracted",
"depends_on": "eval:parent.is_old_subcontracting_flow",
"fieldname": "include_exploded_items",
"fieldtype": "Check",
"label": "Include Exploded Items",
@ -848,6 +852,22 @@
"label": "Sales Order Packed Item",
"no_copy": 1,
"print_hide": 1
},
{
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"fieldname": "fg_item",
"fieldtype": "Link",
"label": "Finished Good Item",
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"options": "Item"
},
{
"default": "1",
"depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow",
"fieldname": "fg_item_qty",
"fieldtype": "Float",
"label": "Finished Good Item Qty",
"mandatory_depends_on": "eval:parent.is_subcontracted && !parent.is_old_subcontracting_flow"
}
],
"idx": 1,

View File

@ -180,12 +180,20 @@ class RequestforQuotation(BuyingController):
doc_args = self.as_dict()
doc_args.update({"supplier": data.get("supplier"), "supplier_name": data.get("supplier_name")})
# Get Contact Full Name
supplier_name = None
if data.get("contact"):
contact_name = frappe.db.get_value(
"Contact", data.get("contact"), ["first_name", "middle_name", "last_name"]
)
supplier_name = (" ").join(x for x in contact_name if x) # remove any blank values
args = {
"update_password_link": update_password_link,
"message": frappe.render_template(self.message_for_supplier, doc_args),
"rfq_link": rfq_link,
"user_fullname": full_name,
"supplier_name": data.get("supplier_name"),
"supplier_name": supplier_name or data.get("supplier_name"),
"supplier_salutation": self.salutation or "Dear Mx.",
}

View File

@ -1,3 +0,0 @@
{% include "erpnext/regional/india/party.js" %}
erpnext.setup_gst_reminder_button('Supplier');

View File

@ -68,9 +68,6 @@ frappe.query_reports["Purchase Analytics"] = {
}
],
after_datatable_render: function(datatable_obj) {
$(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click();
},
get_datatable_options(options) {
return Object.assign(options, {
checkboxColumn: true,
@ -130,11 +127,8 @@ frappe.query_reports["Purchase Analytics"] = {
labels: raw_data.labels,
datasets: new_datasets,
};
chart_options = {
data: new_data,
type: "line",
};
frappe.query_report.render_chart(chart_options);
const new_options = Object.assign({}, frappe.query_report.chart_options, {data: new_data});
frappe.query_report.render_chart(new_options);
frappe.query_report.raw_chart_data = new_data;
},

View File

@ -14,32 +14,29 @@ frappe.query_reports["Subcontract Order Summary"] = {
},
{
label: __("From Date"),
fieldname:"from_date",
fieldname: "from_date",
fieldtype: "Date",
default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
reqd: 1
},
{
label: __("To Date"),
fieldname:"to_date",
fieldname: "to_date",
fieldtype: "Date",
default: frappe.datetime.get_today(),
reqd: 1
},
{
label: __("Purchase Order"),
label: __("Order Type"),
fieldname: "order_type",
fieldtype: "Select",
options: ["Purchase Order", "Subcontracting Order"],
default: "Subcontracting Order"
},
{
label: __("Subcontract Order"),
fieldname: "name",
fieldtype: "Link",
options: "Purchase Order",
get_query: function() {
return {
filters: {
docstatus: 1,
is_subcontracted: 1,
company: frappe.query_report.get_filter_value('company')
}
}
}
fieldtype: "Data"
}
]
};

View File

@ -15,7 +15,7 @@
"name": "Subcontract Order Summary",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"ref_doctype": "Subcontracting Order",
"report_name": "Subcontract Order Summary",
"report_type": "Script Report",
"roles": [

View File

@ -8,7 +8,7 @@ from frappe import _
def execute(filters=None):
columns, data = [], []
columns = get_columns()
columns = get_columns(filters)
data = get_data(filters)
return columns, data
@ -20,43 +20,45 @@ def get_data(report_filters):
if orders:
supplied_items = get_supplied_items(orders, report_filters)
po_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(po_details, data)
order_details = prepare_subcontracted_data(orders, supplied_items)
get_subcontracted_data(order_details, data)
return data
def get_subcontracted_orders(report_filters):
fields = [
"`tabPurchase Order Item`.`parent` as po_id",
"`tabPurchase Order Item`.`item_code`",
"`tabPurchase Order Item`.`item_name`",
"`tabPurchase Order Item`.`qty`",
"`tabPurchase Order Item`.`name`",
"`tabPurchase Order Item`.`received_qty`",
"`tabPurchase Order`.`status`",
f"`tab{report_filters.order_type} Item`.`parent` as order_id",
f"`tab{report_filters.order_type} Item`.`item_code`",
f"`tab{report_filters.order_type} Item`.`item_name`",
f"`tab{report_filters.order_type} Item`.`qty`",
f"`tab{report_filters.order_type} Item`.`name`",
f"`tab{report_filters.order_type} Item`.`received_qty`",
f"`tab{report_filters.order_type}`.`status`",
]
filters = get_filters(report_filters)
return frappe.get_all("Purchase Order", fields=fields, filters=filters) or []
return frappe.get_all(report_filters.order_type, fields=fields, filters=filters) or []
def get_filters(report_filters):
filters = [
["Purchase Order", "docstatus", "=", 1],
["Purchase Order", "is_subcontracted", "=", 1],
[report_filters.order_type, "docstatus", "=", 1],
[
"Purchase Order",
report_filters.order_type,
"transaction_date",
"between",
(report_filters.from_date, report_filters.to_date),
],
]
if report_filters.order_type == "Purchase Order":
filters.append(["Purchase Order", "is_old_subcontracting_flow", "=", 1])
for field in ["name", "company"]:
if report_filters.get(field):
filters.append(["Purchase Order", field, "=", report_filters.get(field)])
filters.append([report_filters.order_type, field, "=", report_filters.get(field)])
return filters
@ -77,10 +79,15 @@ def get_supplied_items(orders, report_filters):
"reference_name",
]
filters = {"parent": ("in", [d.po_id for d in orders]), "docstatus": 1}
filters = {"parent": ("in", [d.order_id for d in orders]), "docstatus": 1}
supplied_items = {}
for row in frappe.get_all("Purchase Order Item Supplied", fields=fields, filters=filters):
supplied_items_table = (
"Purchase Order Item Supplied"
if report_filters.order_type == "Purchase Order"
else "Subcontracting Order Supplied Item"
)
for row in frappe.get_all(supplied_items_table, fields=fields, filters=filters):
new_key = (row.parent, row.reference_name, row.main_item_code)
supplied_items.setdefault(new_key, []).append(row)
@ -89,24 +96,24 @@ def get_supplied_items(orders, report_filters):
def prepare_subcontracted_data(orders, supplied_items):
po_details = {}
order_details = {}
for row in orders:
key = (row.po_id, row.name, row.item_code)
if key not in po_details:
po_details.setdefault(key, frappe._dict({"po_item": row, "supplied_items": []}))
key = (row.order_id, row.name, row.item_code)
if key not in order_details:
order_details.setdefault(key, frappe._dict({"order_item": row, "supplied_items": []}))
details = po_details[key]
details = order_details[key]
if supplied_items.get(key):
for supplied_item in supplied_items[key]:
details["supplied_items"].append(supplied_item)
return po_details
return order_details
def get_subcontracted_data(po_details, data):
for key, details in po_details.items():
res = details.po_item
def get_subcontracted_data(order_details, data):
for key, details in order_details.items():
res = details.order_item
for index, row in enumerate(details.supplied_items):
if index != 0:
res = {}
@ -115,13 +122,13 @@ def get_subcontracted_data(po_details, data):
data.append(res)
def get_columns():
def get_columns(filters):
return [
{
"label": _("Purchase Order"),
"fieldname": "po_id",
"label": _("Subcontract Order"),
"fieldname": "order_id",
"fieldtype": "Link",
"options": "Purchase Order",
"options": filters.order_type,
"width": 100,
},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 80},

View File

@ -4,6 +4,13 @@
frappe.query_reports["Subcontracted Item To Be Received"] = {
"filters": [
{
label: __("Order Type"),
fieldname: "order_type",
fieldtype: "Select",
options: ["Purchase Order", "Subcontracting Order"],
default: "Subcontracting Order"
},
{
fieldname: "supplier",
label: __("Supplier"),

View File

@ -13,7 +13,7 @@
"name": "Subcontracted Item To Be Received",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"ref_doctype": "Subcontracting Order",
"report_name": "Subcontracted Item To Be Received",
"report_type": "Script Report",
"roles": [

View File

@ -11,18 +11,18 @@ def execute(filters=None):
frappe.msgprint(_("To Date must be greater than From Date"))
data = []
columns = get_columns()
columns = get_columns(filters)
get_data(data, filters)
return columns, data
def get_columns():
def get_columns(filters):
return [
{
"label": _("Purchase Order"),
"label": _("Subcontract Order"),
"fieldtype": "Link",
"fieldname": "purchase_order",
"options": "Purchase Order",
"fieldname": "subcontract_order",
"options": filters.order_type,
"width": 150,
},
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "hidden": 1, "width": 150},
@ -57,14 +57,14 @@ def get_columns():
def get_data(data, filters):
po = get_po(filters)
po_name = [v.name for v in po]
sub_items = get_purchase_order_item_supplied(po_name)
for item in sub_items:
for order in po:
orders = get_subcontract_orders(filters)
orders_name = [order.name for order in orders]
subcontracted_items = get_subcontract_order_supplied_item(filters.order_type, orders_name)
for item in subcontracted_items:
for order in orders:
if order.name == item.parent and item.received_qty < item.qty:
row = {
"purchase_order": item.parent,
"subcontract_order": item.parent,
"date": order.transaction_date,
"supplier": order.supplier,
"fg_item_code": item.item_code,
@ -76,22 +76,25 @@ def get_data(data, filters):
data.append(row)
def get_po(filters):
def get_subcontract_orders(filters):
record_filters = [
["is_subcontracted", "=", 1],
["supplier", "=", filters.supplier],
["transaction_date", "<=", filters.to_date],
["transaction_date", ">=", filters.from_date],
["docstatus", "=", 1],
]
if filters.order_type == "Purchase Order":
record_filters.append(["is_old_subcontracting_flow", "=", 1])
return frappe.get_all(
"Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"]
filters.order_type, filters=record_filters, fields=["name", "transaction_date", "supplier"]
)
def get_purchase_order_item_supplied(po):
def get_subcontract_order_supplied_item(order_type, orders):
return frappe.get_all(
"Purchase Order Item",
filters=[("parent", "IN", po)],
f"{order_type} Item",
filters=[("parent", "IN", orders)],
fields=["parent", "item_code", "item_name", "qty", "received_qty"],
)

View File

@ -7,18 +7,35 @@
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.report.subcontracted_item_to_be_received.subcontracted_item_to_be_received import (
execute,
)
from erpnext.controllers.tests.test_subcontracting_controller import (
get_subcontracting_order,
make_service_item,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.subcontracting.doctype.subcontracting_order.subcontracting_order import (
make_subcontracting_receipt,
)
class TestSubcontractedItemToBeReceived(FrappeTestCase):
def test_pending_and_received_qty(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted=1)
transfer_param = []
make_service_item("Subcontracted Service Item 1")
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 500,
"fg_item": "_Test FG Item",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(
service_items=service_items, supplier_warehouse="_Test Warehouse 1 - _TC"
)
make_stock_entry(
item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100
)
@ -28,28 +45,28 @@ class TestSubcontractedItemToBeReceived(FrappeTestCase):
qty=100,
basic_rate=100,
)
make_purchase_receipt_against_po(po.name)
po.reload()
make_subcontracting_receipt_against_sco(sco.name)
sco.reload()
col, data = execute(
filters=frappe._dict(
{
"supplier": po.supplier,
"order_type": "Subcontracting Order",
"supplier": sco.supplier,
"from_date": frappe.utils.get_datetime(
frappe.utils.add_to_date(po.transaction_date, days=-10)
frappe.utils.add_to_date(sco.transaction_date, days=-10)
),
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)),
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)),
}
)
)
self.assertEqual(data[0]["pending_qty"], 5)
self.assertEqual(data[0]["received_qty"], 5)
self.assertEqual(data[0]["purchase_order"], po.name)
self.assertEqual(data[0]["supplier"], po.supplier)
self.assertEqual(data[0]["subcontract_order"], sco.name)
self.assertEqual(data[0]["supplier"], sco.supplier)
def make_purchase_receipt_against_po(po, quantity=5):
pr = make_purchase_receipt(po)
pr.items[0].qty = quantity
pr.supplier_warehouse = "_Test Warehouse 1 - _TC"
pr.insert()
pr.submit()
def make_subcontracting_receipt_against_sco(sco, quantity=5):
scr = make_subcontracting_receipt(sco)
scr.items[0].qty = quantity
scr.insert()
scr.submit()

View File

@ -4,6 +4,13 @@
frappe.query_reports["Subcontracted Raw Materials To Be Transferred"] = {
"filters": [
{
label: __("Order Type"),
fieldname: "order_type",
fieldtype: "Select",
options: ["Purchase Order", "Subcontracting Order"],
default: "Subcontracting Order"
},
{
fieldname: "supplier",
label: __("Supplier"),

View File

@ -13,7 +13,7 @@
"name": "Subcontracted Raw Materials To Be Transferred",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Purchase Order",
"ref_doctype": "Subcontracting Order",
"report_name": "Subcontracted Raw Materials To Be Transferred",
"report_type": "Script Report",
"roles": [

View File

@ -10,19 +10,19 @@ def execute(filters=None):
if filters.from_date >= filters.to_date:
frappe.msgprint(_("To Date must be greater than From Date"))
columns = get_columns()
columns = get_columns(filters)
data = get_data(filters)
return columns, data or []
def get_columns():
def get_columns(filters):
return [
{
"label": _("Purchase Order"),
"label": _("Subcontract Order"),
"fieldtype": "Link",
"fieldname": "purchase_order",
"options": "Purchase Order",
"fieldname": "subcontract_order",
"options": filters.order_type,
"width": 200,
},
{"label": _("Date"), "fieldtype": "Date", "fieldname": "date", "width": 150},
@ -46,10 +46,10 @@ def get_columns():
def get_data(filters):
po_rm_item_details = get_po_items_to_supply(filters)
order_rm_item_details = get_order_items_to_supply(filters)
data = []
for row in po_rm_item_details:
for row in order_rm_item_details:
transferred_qty = row.get("transferred_qty") or 0
if transferred_qty < row.get("reqd_qty", 0):
pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
@ -59,23 +59,33 @@ def get_data(filters):
return data
def get_po_items_to_supply(filters):
def get_order_items_to_supply(filters):
supplied_items_table = (
"Purchase Order Item Supplied"
if filters.order_type == "Purchase Order"
else "Subcontracting Order Supplied Item"
)
record_filters = [
[filters.order_type, "per_received", "<", "100"],
[filters.order_type, "supplier", "=", filters.supplier],
[filters.order_type, "transaction_date", "<=", filters.to_date],
[filters.order_type, "transaction_date", ">=", filters.from_date],
[filters.order_type, "docstatus", "=", 1],
]
if filters.order_type == "Purchase Order":
record_filters.append([filters.order_type, "is_old_subcontracting_flow", "=", 1])
return frappe.db.get_all(
"Purchase Order",
filters.order_type,
fields=[
"name as purchase_order",
"name as subcontract_order",
"transaction_date as date",
"supplier as supplier",
"`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
"`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
"`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty",
],
filters=[
["Purchase Order", "per_received", "<", "100"],
["Purchase Order", "is_subcontracted", "=", 1],
["Purchase Order", "supplier", "=", filters.supplier],
["Purchase Order", "transaction_date", "<=", filters.to_date],
["Purchase Order", "transaction_date", ">=", filters.from_date],
["Purchase Order", "docstatus", "=", 1],
f"`tab{supplied_items_table}`.rm_item_code as rm_item_code",
f"`tab{supplied_items_table}`.required_qty as reqd_qty",
f"`tab{supplied_items_table}`.supplied_qty as transferred_qty",
],
filters=record_filters,
)

View File

@ -3,24 +3,34 @@
# Compiled at: 2019-05-06 10:24:35
# Decompiled by https://python-decompiler.com
import json
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.buying.doctype.purchase_order.purchase_order import make_rm_stock_entry
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.buying.report.subcontracted_raw_materials_to_be_transferred.subcontracted_raw_materials_to_be_transferred import (
execute,
)
from erpnext.controllers.subcontracting_controller import make_rm_stock_entry
from erpnext.controllers.tests.test_subcontracting_controller import (
get_subcontracting_order,
make_service_item,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
class TestSubcontractedItemToBeTransferred(FrappeTestCase):
def test_pending_and_transferred_qty(self):
po = create_purchase_order(
item_code="_Test FG Item", is_subcontracted=1, supplier_warehouse="_Test Warehouse 1 - _TC"
)
make_service_item("Subcontracted Service Item 1")
service_items = [
{
"warehouse": "_Test Warehouse - _TC",
"item_code": "Subcontracted Service Item 1",
"qty": 10,
"rate": 500,
"fg_item": "_Test FG Item",
"fg_item_qty": 10,
},
]
sco = get_subcontracting_order(service_items=service_items)
# Material Receipt of RMs
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=100, basic_rate=100)
@ -28,50 +38,48 @@ class TestSubcontractedItemToBeTransferred(FrappeTestCase):
item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC", qty=100, basic_rate=100
)
se = transfer_subcontracted_raw_materials(po)
transfer_subcontracted_raw_materials(sco)
col, data = execute(
filters=frappe._dict(
{
"supplier": po.supplier,
"order_type": "Subcontracting Order",
"supplier": sco.supplier,
"from_date": frappe.utils.get_datetime(
frappe.utils.add_to_date(po.transaction_date, days=-10)
frappe.utils.add_to_date(sco.transaction_date, days=-10)
),
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10)),
"to_date": frappe.utils.get_datetime(frappe.utils.add_to_date(sco.transaction_date, days=10)),
}
)
)
po.reload()
sco.reload()
po_data = [row for row in data if row.get("purchase_order") == po.name]
sco_data = [row for row in data if row.get("subcontract_order") == sco.name]
# Alphabetically sort to be certain of order
po_data = sorted(po_data, key=lambda i: i["rm_item_code"])
sco_data = sorted(sco_data, key=lambda i: i["rm_item_code"])
self.assertEqual(len(po_data), 2)
self.assertEqual(po_data[0]["purchase_order"], po.name)
self.assertEqual(len(sco_data), 2)
self.assertEqual(sco_data[0]["subcontract_order"], sco.name)
self.assertEqual(po_data[0]["rm_item_code"], "_Test Item")
self.assertEqual(po_data[0]["p_qty"], 8)
self.assertEqual(po_data[0]["transferred_qty"], 2)
self.assertEqual(sco_data[0]["rm_item_code"], "_Test Item")
self.assertEqual(sco_data[0]["p_qty"], 8)
self.assertEqual(sco_data[0]["transferred_qty"], 2)
self.assertEqual(po_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
self.assertEqual(po_data[1]["p_qty"], 19)
self.assertEqual(po_data[1]["transferred_qty"], 1)
se.cancel()
po.cancel()
self.assertEqual(sco_data[1]["rm_item_code"], "_Test Item Home Desktop 100")
self.assertEqual(sco_data[1]["p_qty"], 19)
self.assertEqual(sco_data[1]["transferred_qty"], 1)
def transfer_subcontracted_raw_materials(po):
# Order of supplied items fetched in PO is flaky
def transfer_subcontracted_raw_materials(sco):
# Order of supplied items fetched in SCO is flaky
transfer_qty_map = {"_Test Item": 2, "_Test Item Home Desktop 100": 1}
item_1 = po.supplied_items[0].rm_item_code
item_2 = po.supplied_items[1].rm_item_code
item_1 = sco.supplied_items[0].rm_item_code
item_2 = sco.supplied_items[1].rm_item_code
rm_item = [
rm_items = [
{
"name": po.supplied_items[0].name,
"name": sco.supplied_items[0].name,
"item_code": item_1,
"rm_item_code": item_1,
"item_name": item_1,
@ -82,7 +90,7 @@ def transfer_subcontracted_raw_materials(po):
"stock_uom": "Nos",
},
{
"name": po.supplied_items[1].name,
"name": sco.supplied_items[1].name,
"item_code": item_2,
"rm_item_code": item_2,
"item_name": item_2,
@ -93,8 +101,7 @@ def transfer_subcontracted_raw_materials(po):
"stock_uom": "Nos",
},
]
rm_item_string = json.dumps(rm_item)
se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
se = frappe.get_doc(make_rm_stock_entry(sco.name, rm_items))
se.from_warehouse = "_Test Warehouse - _TC"
se.to_warehouse = "_Test Warehouse - _TC"
se.stock_entry_type = "Send to Subcontractor"

View File

@ -1472,8 +1472,15 @@ class AccountsController(TransactionBase):
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
)
party_account_currency = get_account_currency(party_account)
allow_multi_currency_invoices_against_single_party_account = frappe.db.get_singles_value(
"Accounts Settings", "allow_multi_currency_invoices_against_single_party_account"
)
if not party_gle_currency and (party_account_currency != self.currency):
if (
not party_gle_currency
and (party_account_currency != self.currency)
and not allow_multi_currency_invoices_against_single_party_account
):
frappe.throw(
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
frappe.bold(party_account), party_account_currency, self.currency
@ -2440,7 +2447,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:
@ -2459,6 +2466,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"):
@ -2522,13 +2531,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
@ -2539,6 +2573,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:
@ -2561,6 +2596,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
@ -2574,6 +2610,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
@ -2678,9 +2716,10 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
parent.update_ordered_qty()
parent.update_ordered_and_reserved_qty()
parent.update_receiving_percentage()
if parent.is_subcontracted:
if parent.is_old_subcontracting_flow:
if should_update_supplied_items(parent):
parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied("supplied_items")
parent.create_raw_materials_supplied()
parent.save()
else: # Sales Order
parent.validate_warehouse()

View File

@ -11,8 +11,7 @@ from erpnext.accounts.doctype.budget.budget import validate_expense_against_budg
from erpnext.accounts.party import get_party_details
from erpnext.buying.utils import update_last_purchase_rate, validate_for_items
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.subcontracting import Subcontracting
from erpnext.controllers.subcontracting_controller import SubcontractingController
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.stock.utils import get_incoming_rate
@ -21,7 +20,7 @@ class QtyMismatchError(ValidationError):
pass
class BuyingController(StockController, Subcontracting):
class BuyingController(SubcontractingController):
def __setup__(self):
self.flags.ignore_permlevel_for_fields = ["buying_price_list", "price_list_currency"]
@ -55,7 +54,8 @@ class BuyingController(StockController, Subcontracting):
# sub-contracting
self.validate_for_subcontracting()
self.create_raw_materials_supplied("supplied_items")
if self.get("is_old_subcontracting_flow"):
self.create_raw_materials_supplied()
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
@ -256,6 +256,7 @@ class BuyingController(StockController, Subcontracting):
)
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
if self.get("is_old_subcontracting_flow"):
item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
item.valuation_rate = (
item.base_net_amount
@ -263,6 +264,10 @@ class BuyingController(StockController, Subcontracting):
+ item.rm_supp_cost
+ flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom
else:
item.valuation_rate = (
item.base_net_amount + item.item_tax_amount + flt(item.landed_cost_voucher_amount)
) / qty_in_stock_uom
else:
item.valuation_rate = 0.0
@ -300,7 +305,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")
@ -317,76 +322,25 @@ class BuyingController(StockController, Subcontracting):
d.discount_amount = 0.0
d.margin_rate_or_amount = 0.0
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0
for d in self.get("supplied_items"):
if d.reference_name == item_row_id:
if reset_outgoing_rate and frappe.get_cached_value("Item", d.rm_item_code, "is_stock_item"):
rate = get_incoming_rate(
{
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * d.consumed_qty,
"serial_no": d.serial_no,
"batch_no": d.batch_no,
}
)
if rate > 0:
d.rate = rate
d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
supplied_items_cost += flt(d.amount)
return supplied_items_cost
def validate_for_subcontracting(self):
if self.is_subcontracted:
if self.is_subcontracted and self.get("is_old_subcontracting_flow"):
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
if self.doctype != "Purchase Order":
return
for row in self.get("supplied_items"):
if not row.reserve_warehouse:
msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
frappe.throw(_(msg))
else:
for item in self.get("items"):
if item.bom:
if item.get("bom"):
item.bom = None
def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted:
self.set_materials_for_subcontracted_items(raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
item.rm_supp_cost = 0.0
if not self.is_subcontracted and self.get("supplied_items"):
self.set("supplied_items", [])
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
self._sub_contracted_items = []
item_codes = list(set(item.item_code for item in self.get("items")))
if item_codes:
items = frappe.get_all(
"Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1}
)
self._sub_contracted_items = [item.name for item in items]
return self._sub_contracted_items
def set_qty_as_per_stock_uom(self):
for d in self.get("items"):
if d.meta.get_field("stock_qty"):
@ -510,7 +464,9 @@ class BuyingController(StockController, Subcontracting):
sle.update(
{
"incoming_rate": incoming_rate,
"recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0,
"recalculate_rate": 1
if (self.is_subcontracted and (d.bom or d.fg_item)) or d.from_warehouse
else 0,
}
)
sl_entries.append(sle)
@ -538,6 +494,7 @@ class BuyingController(StockController, Subcontracting):
)
)
if self.get("is_old_subcontracting_flow"):
self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
@ -565,26 +522,9 @@ class BuyingController(StockController, Subcontracting):
)
po_obj.update_ordered_qty(po_item_rows)
if self.is_subcontracted:
if self.get("is_old_subcontracting_flow"):
po_obj.update_reserved_qty_for_subcontract()
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
if hasattr(self, "supplied_items"):
for d in self.get("supplied_items"):
# negative quantity is passed, as raw material qty has to be decreased
# when PR is submitted and it has to be increased when PR is cancelled
sl_entries.append(
self.get_sl_entries(
d,
{
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1 * flt(d.consumed_qty),
"dependant_sle_voucher_detail_no": d.reference_name,
},
)
)
def on_submit(self):
if self.get("is_return"):
return
@ -808,7 +748,7 @@ class BuyingController(StockController, Subcontracting):
if self.doctype == "Material Request":
return
if hasattr(self, "is_subcontracted") and self.is_subcontracted:
if self.get("is_old_subcontracting_flow"):
validate_item_type(self, "is_sub_contracted_item", "subcontracted")
else:
validate_item_type(self, "is_purchase_item", "purchase")

View File

@ -77,7 +77,7 @@ def validate_returned_items(doc):
if doc.doctype != "Purchase Invoice":
select_fields += ",serial_no, batch_no"
if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
select_fields += ",rejected_qty, received_qty"
for d in frappe.db.sql(
@ -161,7 +161,7 @@ def validate_returned_items(doc):
def validate_quantity(doc, args, ref, valid_items, already_returned_items):
fields = ["stock_qty"]
if doc.doctype in ["Purchase Receipt", "Purchase Invoice"]:
if doc.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]:
fields.extend(["received_qty", "rejected_qty"])
already_returned_data = already_returned_items.get(args.item_code) or {}
@ -224,7 +224,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
if ref_item_row.get("rate", 0) > item_dict["rate"]:
item_dict["rate"] = ref_item_row.get("rate", 0)
if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt"]:
if ref_item_row.parenttype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
item_dict["received_qty"] += ref_item_row.received_qty
item_dict["rejected_qty"] += ref_item_row.rejected_qty
@ -239,7 +239,7 @@ def get_ref_item_dict(valid_items, ref_item_row):
def get_already_returned_items(doc):
column = "child.item_code, sum(abs(child.qty)) as qty, sum(abs(child.stock_qty)) as stock_qty"
if doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
if doc.doctype in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
column += """, sum(abs(child.rejected_qty) * child.conversion_factor) as rejected_qty,
sum(abs(child.received_qty) * child.conversion_factor) as received_qty"""
@ -281,17 +281,21 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
if doctype in ("Purchase Receipt", "Purchase Invoice"):
if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
party_type = "supplier"
else:
party_type = "customer"
fields = [
"sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
]
if doctype != "Subcontracting Receipt":
fields += [
"sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype),
]
if doctype in ("Purchase Receipt", "Purchase Invoice"):
if doctype in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
fields += [
"sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
"sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype),
@ -342,7 +346,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
# look for Print Heading "Debit Note"
doc.select_print_heading = frappe.db.get_value("Print Heading", _("Debit Note"))
for tax in doc.get("taxes"):
for tax in doc.get("taxes") or []:
if tax.charge_type == "Actual":
tax.tax_amount = -1 * tax.tax_amount
@ -381,7 +385,10 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
for d in doc.get("packed_items"):
d.qty = d.qty * -1
if doc.get("discount_amount"):
doc.discount_amount = -1 * source.discount_amount
if doctype != "Subcontracting Receipt":
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
@ -393,7 +400,7 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
if serial_nos:
target_doc.serial_no = "\n".join(serial_nos)
if doctype == "Purchase Receipt":
if doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
returned_qty_map = get_returned_qty_map_for_row(
source_parent.name, source_parent.supplier, source_doc.name, doctype
)
@ -405,11 +412,20 @@ def make_return_doc(doctype: str, source_name: str, target_doc=None):
)
target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get("qty") or 0))
target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0))
if hasattr(target_doc, "stock_qty"):
target_doc.stock_qty = -1 * flt(
source_doc.stock_qty - (returned_qty_map.get("stock_qty") or 0)
)
target_doc.received_stock_qty = -1 * flt(
source_doc.received_stock_qty - (returned_qty_map.get("received_stock_qty") or 0)
)
if doctype == "Subcontracting Receipt":
target_doc.subcontracting_order = source_doc.subcontracting_order
target_doc.subcontracting_order_item = source_doc.subcontracting_order_item
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.subcontracting_receipt_item = source_doc.name
else:
target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_order_item = source_doc.purchase_order_item
target_doc.rejected_warehouse = source_doc.rejected_warehouse
@ -525,7 +541,7 @@ def get_rate_for_return(
item_row,
)
if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
if voucher_type in ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"):
select_field = "incoming_rate"
else:
select_field = "abs(stock_value_difference / actual_qty)"
@ -560,6 +576,7 @@ def get_return_against_item_fields(voucher_type):
"Purchase Invoice": "purchase_invoice_item",
"Delivery Note": "dn_detail",
"Sales Invoice": "sales_invoice_item",
"Subcontracting Receipt": "subcontracting_receipt_item",
}
return return_against_item_fields[voucher_type]

View File

@ -1,469 +0,0 @@
import copy
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, flt, get_link_to_form
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class Subcontracting:
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == "Purchase Invoice" and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_purchase_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
)
def __get_purchase_orders(self):
self.purchase_orders = []
if self.doctype == "Purchase Order":
return
self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
def __identify_change_in_item_table(self):
self.__changed_name = []
self.__reference_name = []
if self.doctype == "Purchase Order" or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
for n_row in self.items:
self.__reference_name.append(n_row.name)
if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
self.__changed_name.append(n_row.name)
if item_dict.get(n_row.name):
del item_dict[n_row.name]
self.__changed_name.extend(item_dict.keys())
def __get_data_before_save(self):
item_dict = {}
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self._doc_before_save:
for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
def get_available_materials(self):
"""Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, purchase_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
"""
if not self.purchase_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if key not in self.available_materials:
self.available_materials.setdefault(
key,
frappe._dict(
{
"qty": 0,
"serial_no": [],
"batch_no": defaultdict(float),
"item_details": row,
"po_details": [],
}
),
)
details = self.available_materials[key]
details.qty += row.qty
details.po_details.append(row.po_detail)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
for doctype in ["Purchase Receipt", "Purchase Invoice"]:
self.__update_consumed_materials(doctype)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
"""Deduct the consumed materials from the available materials."""
pr_items = self.__get_received_items(doctype)
if not pr_items:
return ([], {}) if return_consumed_items else None
pr_items = {d.name: d.get(self.get("po_field") or "purchase_order") for d in pr_items}
consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
if return_consumed_items:
return (consumed_materials, pr_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]["qty"] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def __get_transferred_items(self):
fields = ["`tabStock Entry`.`purchase_order`"]
alias_dict = {
"item_code": "rm_item_code",
"subcontracted_item": "main_item_code",
"basic_rate": "rate",
}
child_table_fields = [
"item_code",
"item_name",
"description",
"qty",
"basic_rate",
"amount",
"serial_no",
"uom",
"subcontracted_item",
"stock_uom",
"batch_no",
"conversion_factor",
"s_warehouse",
"t_warehouse",
"item_group",
"po_detail",
]
if self.backflush_based_on == "BOM":
child_table_fields.append("original_item")
for field in child_table_fields:
fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
filters = [
["Stock Entry", "docstatus", "=", 1],
["Stock Entry", "purpose", "=", "Send to Subcontractor"],
["Stock Entry", "purchase_order", "in", self.purchase_orders],
]
return frappe.get_all("Stock Entry", fields=fields, filters=filters)
def __get_received_items(self, doctype):
fields = []
self.po_field = "purchase_order"
for field in ["name", self.po_field, "parent"]:
fields.append(f"`tab{doctype} Item`.`{field}`")
filters = [
[doctype, "docstatus", "=", 1],
[f"{doctype} Item", self.po_field, "in", self.purchase_orders],
]
if doctype == "Purchase Invoice":
filters.append(["Purchase Invoice", "update_stock", "=", 1])
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
def __get_consumed_items(self, doctype, pr_items):
return frappe.get_all(
"Purchase Receipt Item Supplied",
fields=[
"serial_no",
"rm_item_code",
"reference_name",
"batch_no",
"consumed_qty",
"main_item_code",
],
filters={"docstatus": 1, "reference_name": ("in", list(pr_items)), "parenttype": doctype},
)
def __set_alternative_item_details(self, row):
if row.get("original_item"):
self.alternative_item_details[row.get("original_item")] = row
def __get_pending_qty_to_receive(self):
"""Get qty to be received against the purchase order."""
self.qty_to_be_received = defaultdict(float)
if (
self.doctype != "Purchase Order" and self.backflush_based_on != "BOM" and self.purchase_orders
):
for row in frappe.get_all(
"Purchase Order Item",
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
filters={"docstatus": 1, "parent": ("in", self.purchase_orders)},
):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
alias_dict = {
"item_code": "rm_item_code",
"name": "bom_detail_no",
"source_warehouse": "reserve_warehouse",
}
for field in [
"item_code",
"name",
"rate",
"stock_uom",
"source_warehouse",
"description",
"item_name",
"stock_uom",
]:
fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
filters = [
[doctype, "parent", "=", bom_no],
[doctype, "docstatus", "=", 1],
["BOM", "item", "=", item_code],
[doctype, "sourced_by_supplier", "=", 0],
]
return (
frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
)
def __remove_changed_rows(self):
if not self.__changed_name:
return
i = 1
self.set(self.raw_material_table, [])
for d in self._doc_before_save.supplied_items:
if d.reference_name in self.__changed_name:
continue
if d.reference_name not in self.__reference_name:
continue
d.idx = i
self.append("supplied_items", d)
i += 1
def __set_supplied_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if self.doctype != "Purchase Order" and (
(self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name)
):
continue
if self.doctype == "Purchase Order" or self.backflush_based_on == "BOM":
for bom_item in self.__get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != "BOM":
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
if self.qty_to_be_received:
self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
def __update_reserve_warehouse(self, row, item):
if self.doctype == "Purchase Order":
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.purchase_order)
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
"UOM", transfer_item.item_details.stock_uom, "must_be_whole_number"
):
return frappe.utils.ceil(qty)
return qty
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == "Purchase Order":
rm_obj.required_qty = qty
else:
rm_obj.consumed_qty = 0
rm_obj.purchase_order = item_row.purchase_order
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]["batch_no"][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update(
{
"consumed_qty": qty,
"batch_no": batch_no,
"required_qty": qty,
"purchase_order": item_row.purchase_order,
}
)
self.__set_serial_nos(item_row, rm_obj)
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
rm_obj.serial_no = "\n".join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]["serial_no"].remove(sn)
def set_consumed_qty_in_po(self):
# Update consumed qty back in the purchase order
if not self.is_subcontracted:
return
self.__get_purchase_orders()
itemwise_consumed_qty = defaultdict(float)
for doctype in ["Purchase Receipt", "Purchase Invoice"]:
consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_po(itemwise_consumed_qty)
def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
filters = {"docstatus": 1, "parent": ("in", self.purchase_orders)}
for row in frappe.get_all(
"Purchase Order Item Supplied", fields=fields, filters=filters, order_by="idx"
):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value("Purchase Order Item Supplied", row.name, "consumed_qty", consumed_qty)
def __validate_supplied_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt"]:
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.purchase_order)
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
"batch_no"
):
link = get_link_to_form("Purchase Order", row.purchase_order)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_no"):
serial_nos = get_serial_nos(row.get("serial_no"))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form("Purchase Order", row.purchase_order)
msg = f"The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))

View File

@ -0,0 +1,902 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import copy
import json
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, cstr, flt, get_link_to_form
from erpnext.controllers.stock_controller import StockController
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.utils import get_incoming_rate
class SubcontractingController(StockController):
def __init__(self, *args, **kwargs):
super(SubcontractingController, self).__init__(*args, **kwargs)
if self.get("is_old_subcontracting_flow"):
self.subcontract_data = frappe._dict(
{
"order_doctype": "Purchase Order",
"order_field": "purchase_order",
"rm_detail_field": "po_detail",
"receipt_supplied_items_field": "Purchase Receipt Item Supplied",
"order_supplied_items_field": "Purchase Order Item Supplied",
}
)
else:
self.subcontract_data = frappe._dict(
{
"order_doctype": "Subcontracting Order",
"order_field": "subcontracting_order",
"rm_detail_field": "sco_rm_detail",
"receipt_supplied_items_field": "Subcontracting Receipt Supplied Item",
"order_supplied_items_field": "Subcontracting Order Supplied Item",
}
)
def before_validate(self):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.remove_empty_rows()
self.set_items_conversion_factor()
def validate(self):
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"]:
self.validate_items()
self.create_raw_materials_supplied()
else:
super(SubcontractingController, self).validate()
def remove_empty_rows(self):
for key in ["service_items", "items", "supplied_items"]:
if self.get(key):
idx = 1
for item in self.get(key)[:]:
if not (item.get("item_code") or item.get("main_item_code")):
self.get(key).remove(item)
else:
item.idx = idx
idx += 1
def set_items_conversion_factor(self):
for item in self.get("items"):
if not item.conversion_factor:
item.conversion_factor = 1
def validate_items(self):
for item in self.items:
if not frappe.get_value("Item", item.item_code, "is_sub_contracted_item"):
msg = f"Item {item.item_name} must be a subcontracted item."
frappe.throw(_(msg))
if item.bom:
bom = frappe.get_doc("BOM", item.bom)
if not bom.is_active:
msg = f"Please select an active BOM for Item {item.item_name}."
frappe.throw(_(msg))
if bom.item != item.item_code:
msg = f"Please select an valid BOM for Item {item.item_name}."
frappe.throw(_(msg))
def __get_data_before_save(self):
item_dict = {}
if (
self.doctype in ["Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"]
and self._doc_before_save
):
for row in self._doc_before_save.get("items"):
item_dict[row.name] = (row.item_code, row.qty)
return item_dict
def __identify_change_in_item_table(self):
self.__changed_name = []
self.__reference_name = []
if self.doctype in ["Purchase Order", "Subcontracting Order"] or self.is_new():
self.set(self.raw_material_table, [])
return
item_dict = self.__get_data_before_save()
if not item_dict:
return True
for row in self.items:
self.__reference_name.append(row.name)
if (row.name not in item_dict) or (row.item_code, row.qty) != item_dict[row.name]:
self.__changed_name.append(row.name)
if item_dict.get(row.name):
del item_dict[row.name]
self.__changed_name.extend(item_dict.keys())
def __get_backflush_based_on(self):
self.backflush_based_on = frappe.db.get_single_value(
"Buying Settings", "backflush_raw_materials_of_subcontract_based_on"
)
def initialized_fields(self):
self.available_materials = frappe._dict()
self.__transferred_items = frappe._dict()
self.alternative_item_details = frappe._dict()
self.__get_backflush_based_on()
def __get_subcontract_orders(self):
self.subcontract_orders = []
if self.doctype in ["Purchase Order", "Subcontracting Order"]:
return
self.subcontract_orders = [
item.get(self.subcontract_data.order_field)
for item in self.items
if item.get(self.subcontract_data.order_field)
]
def __get_pending_qty_to_receive(self):
"""Get qty to be received against the subcontract order."""
self.qty_to_be_received = defaultdict(float)
if (
self.doctype != self.subcontract_data.order_doctype
and self.backflush_based_on != "BOM"
and self.subcontract_orders
):
for row in frappe.get_all(
f"{self.subcontract_data.order_doctype} Item",
fields=["item_code", "(qty - received_qty) as qty", "parent", "name"],
filters={"docstatus": 1, "parent": ("in", self.subcontract_orders)},
):
self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
def __get_transferred_items(self):
fields = [f"`tabStock Entry`.`{self.subcontract_data.order_field}`"]
alias_dict = {
"item_code": "rm_item_code",
"subcontracted_item": "main_item_code",
"basic_rate": "rate",
}
child_table_fields = [
"item_code",
"item_name",
"description",
"qty",
"basic_rate",
"amount",
"serial_no",
"uom",
"subcontracted_item",
"stock_uom",
"batch_no",
"conversion_factor",
"s_warehouse",
"t_warehouse",
"item_group",
self.subcontract_data.rm_detail_field,
]
if self.backflush_based_on == "BOM":
child_table_fields.append("original_item")
for field in child_table_fields:
fields.append(f"`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}")
filters = [
["Stock Entry", "docstatus", "=", 1],
["Stock Entry", "purpose", "=", "Send to Subcontractor"],
["Stock Entry", self.subcontract_data.order_field, "in", self.subcontract_orders],
]
return frappe.get_all("Stock Entry", fields=fields, filters=filters)
def __set_alternative_item_details(self, row):
if row.get("original_item"):
self.alternative_item_details[row.get("original_item")] = row
def __get_received_items(self, doctype):
fields = []
for field in ["name", self.subcontract_data.order_field, "parent"]:
fields.append(f"`tab{doctype} Item`.`{field}`")
filters = [
[doctype, "docstatus", "=", 1],
[f"{doctype} Item", self.subcontract_data.order_field, "in", self.subcontract_orders],
]
if doctype == "Purchase Invoice":
filters.append(["Purchase Invoice", "update_stock", "=", 1])
return frappe.get_all(f"{doctype}", fields=fields, filters=filters)
def __get_consumed_items(self, doctype, receipt_items):
return frappe.get_all(
self.subcontract_data.receipt_supplied_items_field,
fields=[
"serial_no",
"rm_item_code",
"reference_name",
"batch_no",
"consumed_qty",
"main_item_code",
],
filters={"docstatus": 1, "reference_name": ("in", list(receipt_items)), "parenttype": doctype},
)
def __update_consumed_materials(self, doctype, return_consumed_items=False):
"""Deduct the consumed materials from the available materials."""
receipt_items = self.__get_received_items(doctype)
if not receipt_items:
return ([], {}) if return_consumed_items else None
receipt_items = {
item.name: item.get(self.subcontract_data.order_field) for item in receipt_items
}
consumed_materials = self.__get_consumed_items(doctype, receipt_items.keys())
if return_consumed_items:
return (consumed_materials, receipt_items)
for row in consumed_materials:
key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name))
if not self.available_materials.get(key):
continue
self.available_materials[key]["qty"] -= row.consumed_qty
if row.serial_no:
self.available_materials[key]["serial_no"] = list(
set(self.available_materials[key]["serial_no"]) - set(get_serial_nos(row.serial_no))
)
if row.batch_no:
self.available_materials[key]["batch_no"][row.batch_no] -= row.consumed_qty
def get_available_materials(self):
"""Get the available raw materials which has been transferred to the supplier.
available_materials = {
(item_code, subcontracted_item, subcontract_order): {
'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
}
}
"""
if not self.subcontract_orders:
return
for row in self.__get_transferred_items():
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if key not in self.available_materials:
self.available_materials.setdefault(
key,
frappe._dict(
{
"qty": 0,
"serial_no": [],
"batch_no": defaultdict(float),
"item_details": row,
f"{self.subcontract_data.rm_detail_field}s": [],
}
),
)
details = self.available_materials[key]
details.qty += row.qty
details[f"{self.subcontract_data.rm_detail_field}s"].append(
row.get(self.subcontract_data.rm_detail_field)
)
if row.serial_no:
details.serial_no.extend(get_serial_nos(row.serial_no))
if row.batch_no:
details.batch_no[row.batch_no] += row.qty
self.__set_alternative_item_details(row)
self.__transferred_items = copy.deepcopy(self.available_materials)
if self.get("is_old_subcontracting_flow"):
for doctype in ["Purchase Receipt", "Purchase Invoice"]:
self.__update_consumed_materials(doctype)
else:
self.__update_consumed_materials("Subcontracting Receipt")
def __remove_changed_rows(self):
if not self.__changed_name:
return
i = 1
self.set(self.raw_material_table, [])
for item in self._doc_before_save.supplied_items:
if item.reference_name in self.__changed_name:
continue
if item.reference_name not in self.__reference_name:
continue
item.idx = i
self.append("supplied_items", item)
i += 1
def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
fields = [f"`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit"]
alias_dict = {
"item_code": "rm_item_code",
"name": "bom_detail_no",
"source_warehouse": "reserve_warehouse",
}
for field in [
"item_code",
"name",
"rate",
"stock_uom",
"source_warehouse",
"description",
"item_name",
"stock_uom",
]:
fields.append(f"`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}")
filters = [
[doctype, "parent", "=", bom_no],
[doctype, "docstatus", "=", 1],
["BOM", "item", "=", item_code],
[doctype, "sourced_by_supplier", "=", 0],
]
return (
frappe.get_all("BOM", fields=fields, filters=filters, order_by=f"`tab{doctype}`.`idx`") or []
)
def __update_reserve_warehouse(self, row, item):
if self.doctype == self.subcontract_data.order_doctype:
row.reserve_warehouse = self.set_reserve_warehouse or item.warehouse
def __set_alternative_item(self, bom_item):
if self.alternative_item_details.get(bom_item.rm_item_code):
bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
def __set_serial_nos(self, item_row, rm_obj):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.available_materials.get(key) and self.available_materials[key]["serial_no"]:
used_serial_nos = self.available_materials[key]["serial_no"][0 : cint(rm_obj.consumed_qty)]
rm_obj.serial_no = "\n".join(used_serial_nos)
# Removed the used serial nos from the list
for sn in used_serial_nos:
self.available_materials[key]["serial_no"].remove(sn)
def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
rm_obj.update(
{
"consumed_qty": qty,
"batch_no": batch_no,
"required_qty": qty,
self.subcontract_data.order_field: item_row.get(self.subcontract_data.order_field),
}
)
self.__set_serial_nos(item_row, rm_obj)
def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
rm_obj.required_qty = required_qty
rm_obj.consumed_qty = consumed_qty
def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
key = (rm_obj.rm_item_code, item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.available_materials.get(key) and self.available_materials[key]["batch_no"]:
new_rm_obj = None
for batch_no, batch_qty in self.available_materials[key]["batch_no"].items():
if batch_qty >= qty:
self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
self.available_materials[key]["batch_no"][batch_no] -= qty
return
elif qty > 0 and batch_qty > 0:
qty -= batch_qty
new_rm_obj = self.append(self.raw_material_table, bom_item)
new_rm_obj.reference_name = item_row.name
self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
self.available_materials[key]["batch_no"][batch_no] = 0
if abs(qty) > 0 and not new_rm_obj:
self.__set_consumed_qty(rm_obj, qty)
else:
self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
self.__set_serial_nos(item_row, rm_obj)
def __add_supplied_item(self, item_row, bom_item, qty):
bom_item.conversion_factor = item_row.conversion_factor
rm_obj = self.append(self.raw_material_table, bom_item)
rm_obj.reference_name = item_row.name
if self.doctype == "Subcontracting Receipt":
args = frappe._dict(
{
"item_code": rm_obj.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * flt(rm_obj.consumed_qty),
"serial_no": rm_obj.serial_no,
"batch_no": rm_obj.batch_no,
"voucher_type": self.doctype,
"voucher_no": self.name,
"company": self.company,
"allow_zero_valuation": 1,
}
)
rm_obj.rate = get_incoming_rate(args)
if self.doctype == self.subcontract_data.order_doctype:
rm_obj.required_qty = qty
rm_obj.amount = rm_obj.required_qty * rm_obj.rate
else:
rm_obj.consumed_qty = 0
setattr(
rm_obj, self.subcontract_data.order_field, item_row.get(self.subcontract_data.order_field)
)
self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
key = (item_row.item_code, item_row.get(self.subcontract_data.order_field))
if self.qty_to_be_received == item_row.qty:
return transfer_item.qty
if self.qty_to_be_received:
qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
transfer_item.item_details.required_qty = transfer_item.qty
if transfer_item.serial_no or frappe.get_cached_value(
"UOM", transfer_item.item_details.stock_uom, "must_be_whole_number"
):
return frappe.utils.ceil(qty)
return qty
def __set_supplied_items(self):
self.bom_items = {}
has_supplied_items = True if self.get(self.raw_material_table) else False
for row in self.items:
if self.doctype != self.subcontract_data.order_doctype and (
(self.__changed_name and row.name not in self.__changed_name)
or (has_supplied_items and not self.__changed_name)
):
continue
if self.doctype == self.subcontract_data.order_doctype or self.backflush_based_on == "BOM":
for bom_item in self.__get_materials_from_bom(
row.item_code, row.bom, row.get("include_exploded_items")
):
qty = flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor
bom_item.main_item_code = row.item_code
self.__update_reserve_warehouse(bom_item, row)
self.__set_alternative_item(bom_item)
self.__add_supplied_item(row, bom_item, qty)
elif self.backflush_based_on != "BOM":
for key, transfer_item in self.available_materials.items():
if (key[1], key[2]) == (
row.item_code,
row.get(self.subcontract_data.order_field),
) and transfer_item.qty > 0:
qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
transfer_item.qty -= qty
self.__add_supplied_item(row, transfer_item.get("item_details"), qty)
if self.qty_to_be_received:
self.qty_to_be_received[
(row.item_code, row.get(self.subcontract_data.order_field))
] -= row.qty
def __prepare_supplied_items(self):
self.initialized_fields()
self.__get_subcontract_orders()
self.__get_pending_qty_to_receive()
self.get_available_materials()
self.__remove_changed_rows()
self.__set_supplied_items()
def __validate_batch_no(self, row, key):
if row.get("batch_no") and row.get("batch_no") not in self.__transferred_items.get(key).get(
"batch_no"
):
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the {self.subcontract_data.order_doctype} {link}'
frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
def __validate_serial_no(self, row, key):
if row.get("serial_no"):
serial_nos = get_serial_nos(row.get("serial_no"))
incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get("serial_no"))
if incorrect_sn:
incorrect_sn = "\n".join(incorrect_sn)
link = get_link_to_form(
self.subcontract_data.order_doctype, row.get(self.subcontract_data.order_field)
)
msg = f"The Serial Nos {incorrect_sn} has not supplied against the {self.subcontract_data.order_doctype} {link}"
frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
def __validate_supplied_items(self):
if self.doctype not in ["Purchase Invoice", "Purchase Receipt", "Subcontracting Receipt"]:
return
for row in self.get(self.raw_material_table):
key = (row.rm_item_code, row.main_item_code, row.get(self.subcontract_data.order_field))
if not self.__transferred_items or not self.__transferred_items.get(key):
return
self.__validate_batch_no(row, key)
self.__validate_serial_no(row, key)
def set_materials_for_subcontracted_items(self, raw_material_table):
if self.doctype == "Purchase Invoice" and not self.update_stock:
return
self.raw_material_table = raw_material_table
self.__identify_change_in_item_table()
self.__prepare_supplied_items()
self.__validate_supplied_items()
def create_raw_materials_supplied(self, raw_material_table="supplied_items"):
self.set_materials_for_subcontracted_items(raw_material_table)
if self.doctype in ["Subcontracting Receipt", "Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
item.rm_supp_cost = 0.0
def __update_consumed_qty_in_subcontract_order(self, itemwise_consumed_qty):
fields = ["main_item_code", "rm_item_code", "parent", "supplied_qty", "name"]
filters = {"docstatus": 1, "parent": ("in", self.subcontract_orders)}
for row in frappe.get_all(
self.subcontract_data.order_supplied_items_field, fields=fields, filters=filters, order_by="idx"
):
key = (row.rm_item_code, row.main_item_code, row.parent)
consumed_qty = itemwise_consumed_qty.get(key, 0)
if row.supplied_qty < consumed_qty:
consumed_qty = row.supplied_qty
itemwise_consumed_qty[key] -= consumed_qty
frappe.db.set_value(
self.subcontract_data.order_supplied_items_field, row.name, "consumed_qty", consumed_qty
)
def set_consumed_qty_in_subcontract_order(self):
# Update consumed qty back in the subcontract order
if self.doctype in ["Subcontracting Order", "Subcontracting Receipt"] or self.get(
"is_old_subcontracting_flow"
):
self.__get_subcontract_orders()
itemwise_consumed_qty = defaultdict(float)
if self.get("is_old_subcontracting_flow"):
doctypes = ["Purchase Receipt", "Purchase Invoice"]
else:
doctypes = ["Subcontracting Receipt"]
for doctype in doctypes:
consumed_items, receipt_items = self.__update_consumed_materials(
doctype, return_consumed_items=True
)
for row in consumed_items:
key = (row.rm_item_code, row.main_item_code, receipt_items.get(row.reference_name))
itemwise_consumed_qty[key] += row.consumed_qty
self.__update_consumed_qty_in_subcontract_order(itemwise_consumed_qty)
def update_ordered_and_reserved_qty(self):
sco_map = {}
for item in self.get("items"):
if self.doctype == "Subcontracting Receipt" and item.subcontracting_order:
sco_map.setdefault(item.subcontracting_order, []).append(item.subcontracting_order_item)
for sco, sco_item_rows in sco_map.items():
if sco and sco_item_rows:
sco_doc = frappe.get_doc("Subcontracting Order", sco)
if sco_doc.status in ["Closed", "Cancelled"]:
frappe.throw(
_("{0} {1} is cancelled or closed").format(_("Subcontracting Order"), sco),
frappe.InvalidStatusError,
)
sco_doc.update_ordered_qty_for_subcontracting(sco_item_rows)
sco_doc.update_reserved_qty_for_subcontracting()
def make_sl_entries_for_supplier_warehouse(self, sl_entries):
if hasattr(self, "supplied_items"):
for item in self.get("supplied_items"):
# negative quantity is passed, as raw material qty has to be decreased
# when SCR is submitted and it has to be increased when SCR is cancelled
sl_entries.append(
self.get_sl_entries(
item,
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1 * flt(item.consumed_qty),
"dependant_sle_voucher_detail_no": item.reference_name,
},
)
)
def update_stock_ledger(self, allow_negative_stock=False, via_landed_cost_voucher=False):
self.update_ordered_and_reserved_qty()
sl_entries = []
stock_items = self.get_stock_items()
for item in self.get("items"):
if item.item_code in stock_items and item.warehouse:
scr_qty = flt(item.qty) * flt(item.conversion_factor)
if scr_qty:
sle = self.get_sl_entries(
item, {"actual_qty": flt(scr_qty), "serial_no": cstr(item.serial_no).strip()}
)
rate_db_precision = 6 if cint(self.precision("rate", item)) <= 6 else 9
incoming_rate = flt(item.rate, rate_db_precision)
sle.update(
{
"incoming_rate": incoming_rate,
"recalculate_rate": 1,
}
)
sl_entries.append(sle)
if flt(item.rejected_qty) != 0:
sl_entries.append(
self.get_sl_entries(
item,
{
"warehouse": item.rejected_warehouse,
"actual_qty": flt(item.rejected_qty) * flt(item.conversion_factor),
"serial_no": cstr(item.rejected_serial_no).strip(),
"incoming_rate": 0.0,
"recalculate_rate": 1,
},
)
)
self.make_sl_entries_for_supplier_warehouse(sl_entries)
self.make_sl_entries(
sl_entries,
allow_negative_stock=allow_negative_stock,
via_landed_cost_voucher=via_landed_cost_voucher,
)
def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
supplied_items_cost = 0.0
for item in self.get("supplied_items"):
if item.reference_name == item_row_id:
if (
self.get("is_old_subcontracting_flow")
and reset_outgoing_rate
and frappe.get_cached_value("Item", item.rm_item_code, "is_stock_item")
):
rate = get_incoming_rate(
{
"item_code": item.rm_item_code,
"warehouse": self.supplier_warehouse,
"posting_date": self.posting_date,
"posting_time": self.posting_time,
"qty": -1 * item.consumed_qty,
"serial_no": item.serial_no,
"batch_no": item.batch_no,
}
)
if rate > 0:
item.rate = rate
item.amount = flt(flt(item.consumed_qty) * flt(item.rate), item.precision("amount"))
supplied_items_cost += item.amount
return supplied_items_cost
def set_subcontracting_order_status(self):
if self.doctype == "Subcontracting Order":
self.update_status()
elif self.doctype == "Subcontracting Receipt":
self.__get_subcontract_orders
if self.subcontract_orders:
for sco in set(self.subcontract_orders):
sco_doc = frappe.get_doc("Subcontracting Order", sco)
sco_doc.update_status()
@frappe.whitelist()
def get_current_stock(self):
if self.doctype in ["Purchase Receipt", "Subcontracting Receipt"]:
for item in self.get("supplied_items"):
if self.supplier_warehouse:
actual_qty = frappe.db.get_value(
"Bin",
{"item_code": item.rm_item_code, "warehouse": self.supplier_warehouse},
"actual_qty",
)
item.current_stock = flt(actual_qty) or 0
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
self._sub_contracted_items = []
item_codes = list(set(item.item_code for item in self.get("items")))
if item_codes:
items = frappe.get_all(
"Item", filters={"name": ["in", item_codes], "is_sub_contracted_item": 1}
)
self._sub_contracted_items = [item.name for item in items]
return self._sub_contracted_items
def get_item_details(items):
item = frappe.qb.DocType("Item")
item_list = (
frappe.qb.from_(item)
.select(item.item_code, item.description, item.allow_alternative_item)
.where(item.name.isin(items))
.run(as_dict=True)
)
item_details = {}
for item in item_list:
item_details[item.item_code] = item
return item_details
@frappe.whitelist()
def make_rm_stock_entry(subcontract_order, rm_items, order_doctype="Subcontracting Order"):
rm_items_list = rm_items
if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
fg_items = list(set(item["item_code"] for item in rm_items_list))
else:
frappe.throw(_("No Items selected for transfer"))
if subcontract_order:
subcontract_order = frappe.get_doc(order_doctype, subcontract_order)
if fg_items:
items = tuple(set(item["rm_item_code"] for item in rm_items_list))
item_wh = get_item_details(items)
stock_entry = frappe.new_doc("Stock Entry")
stock_entry.purpose = "Send to Subcontractor"
if order_doctype == "Purchase Order":
stock_entry.purchase_order = subcontract_order.name
else:
stock_entry.subcontracting_order = subcontract_order.name
stock_entry.supplier = subcontract_order.supplier
stock_entry.supplier_name = subcontract_order.supplier_name
stock_entry.supplier_address = subcontract_order.supplier_address
stock_entry.address_display = subcontract_order.address_display
stock_entry.company = subcontract_order.company
stock_entry.to_warehouse = subcontract_order.supplier_warehouse
stock_entry.set_stock_entry_type()
if order_doctype == "Purchase Order":
rm_detail_field = "po_detail"
else:
rm_detail_field = "sco_rm_detail"
for item_code in fg_items:
for rm_item_data in rm_items_list:
if rm_item_data["item_code"] == item_code:
rm_item_code = rm_item_data["rm_item_code"]
items_dict = {
rm_item_code: {
rm_detail_field: rm_item_data.get("name"),
"item_name": rm_item_data["item_name"],
"description": item_wh.get(rm_item_code, {}).get("description", ""),
"qty": rm_item_data["qty"],
"from_warehouse": rm_item_data["warehouse"],
"stock_uom": rm_item_data["stock_uom"],
"serial_no": rm_item_data.get("serial_no"),
"batch_no": rm_item_data.get("batch_no"),
"main_item_code": rm_item_data["item_code"],
"allow_alternative_item": item_wh.get(rm_item_code, {}).get("allow_alternative_item"),
}
}
stock_entry.add_to_stock_entry_detail(items_dict)
return stock_entry.as_dict()
else:
frappe.throw(_("No Items selected for transfer"))
return subcontract_order.name
def add_items_in_ste(
ste_doc, row, qty, rm_details, rm_detail_field="sco_rm_detail", batch_no=None
):
item = ste_doc.append("items", row.item_details)
rm_detail = list(set(row.get(f"{rm_detail_field}s")).intersection(rm_details))
item.update(
{
"qty": qty,
"batch_no": batch_no,
"basic_rate": row.item_details["rate"],
rm_detail_field: rm_detail[0] if rm_detail else "",
"s_warehouse": row.item_details["t_warehouse"],
"t_warehouse": row.item_details["s_warehouse"],
"item_code": row.item_details["rm_item_code"],
"subcontracted_item": row.item_details["main_item_code"],
"serial_no": "\n".join(row.serial_no) if row.serial_no else "",
}
)
def make_return_stock_entry_for_subcontract(
available_materials, order_doc, rm_details, order_doctype="Subcontracting Order"
):
ste_doc = frappe.new_doc("Stock Entry")
ste_doc.purpose = "Material Transfer"
if order_doctype == "Purchase Order":
ste_doc.purchase_order = order_doc.name
rm_detail_field = "po_detail"
else:
ste_doc.subcontracting_order = order_doc.name
rm_detail_field = "sco_rm_detail"
ste_doc.company = order_doc.company
ste_doc.is_return = 1
for key, value in available_materials.items():
if not value.qty:
continue
if value.batch_no:
for batch_no, qty in value.batch_no.items():
if qty > 0:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field, batch_no)
else:
add_items_in_ste(ste_doc, value, value.qty, rm_details, rm_detail_field)
ste_doc.set_stock_entry_type()
ste_doc.calculate_rate_and_amount()
return ste_doc
@frappe.whitelist()
def get_materials_from_supplier(
subcontract_order, rm_details, order_doctype="Subcontracting Order"
):
if isinstance(rm_details, str):
rm_details = json.loads(rm_details)
doc = frappe.get_cached_doc(order_doctype, subcontract_order)
doc.initialized_fields()
doc.subcontract_orders = [doc.name]
doc.get_available_materials()
if not doc.available_materials:
frappe.throw(
_("Materials are already received against the {0} {1}").format(order_doctype, subcontract_order)
)
return make_return_stock_entry_for_subcontract(
doc.available_materials, doc, rm_details, order_doctype
)

View File

@ -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")
)

File diff suppressed because it is too large Load Diff

View 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": []
}

View File

@ -5,5 +5,5 @@
from frappe.model.document import Document
class HSNTaxRate(Document):
class CRMNote(Document):
pass

View File

@ -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
}

View File

@ -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"));
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
});
}
}
});

View File

@ -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"
}

View File

@ -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 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
)
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.update_links()
self.link_to_contact()
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 on_update(self):
self.update_prospect()
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 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.salutation, self.first_name, self.middle_name, self.last_name])
)
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_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 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,21 +183,16 @@ 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()
@ -197,7 +204,7 @@ class Lead(SellingController):
"last_name": self.last_name,
"salutation": self.salutation,
"gender": self.gender,
"designation": self.designation,
"job_title": self.job_title,
"company_name": self.company_name,
}
)
@ -216,6 +223,39 @@ class Lead(SellingController):
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",
{
"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,
},
)
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()
def make_customer(source_name, target_doc=None):
@ -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",
)

View File

@ -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);

View File

@ -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()

View File

@ -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}));

View File

@ -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",

View File

@ -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.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":
if self.party_name:
if 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":
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"]
)
self.customer_name = company_name or lead_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

View File

@ -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'
}

View File

@ -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(),
}
)

View File

@ -8,7 +8,9 @@
"transaction_date": "2013-12-12",
"items": [{
"item_name": "Test Item",
"description": "Some description"
"description": "Some description",
"qty": 5,
"rate": 100
}]
}
]

View File

@ -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();
}
});

View File

@ -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
}

View File

@ -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)
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
carry_forward_communication_and_comments = frappe.db.get_single_value(
"CRM Settings", "carry_forward_communication_and_comments"
)
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",
],
)

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