Merge branch 'develop' of https://github.com/frappe/erpnext into move-exotel-to-separate-app

This commit is contained in:
Suraj Shetty 2022-07-22 12:24:20 +05:30
commit 3593573ed2
1603 changed files with 32272 additions and 158414 deletions

View File

@ -5,7 +5,7 @@
"es6": true
},
"parserOptions": {
"ecmaVersion": 9,
"ecmaVersion": 11,
"sourceType": "module"
},
"extends": "eslint:recommended",

View File

@ -12,10 +12,18 @@ Welcome to ERPNext issue tracker! Before creating an issue, please heed the foll
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
3. When making a feature request, make sure to be as verbose as possible. The better you convey your message, the greater the drive to make it happen.
Please keep in mind that we get many many requests and we can't possibly work on all of them, we prioritize development based on the goals of the product and organization. Feature requests are still welcome as it helps us in research when we do decide to work on the requested feature.
If you're in urgent need to a feature, please try the following channels to get paid developments done quickly:
1. Certified ERPNext partners: https://erpnext.com/partners
2. Developer community on ERPNext forums: https://discuss.erpnext.com/c/developers/5
3. Telegram group for ERPNext/Frappe development work: https://t.me/erpnext_opps
-->
**Is your feature request related to a problem? Please describe.**

View File

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

View File

@ -11,7 +11,7 @@ fi
cd ~ || exit
sudo apt-get install redis-server libcups2-dev
sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench

12
.github/stale.yml vendored
View File

@ -24,14 +24,4 @@ pulls:
:) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- valid
- to-validate
- QA
markComment: >
This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be
closed within a week if no further activity occurs.
only: pulls

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,4 +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"
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

@ -9,6 +9,8 @@ pull_request_rules:
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=mergify[bot]
- or:
- base=version-13
- base=version-12
@ -19,6 +21,16 @@ pull_request_rules:
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
- name: Auto-close PRs on pre-release branch
conditions:
- base=version-13-pre-release
actions:
close:
comment:
message: |
@{{author}}, pre-release branch is not maintained anymore. Releases are directly done by merging hotfix branch to stable branches.
- name: backport to develop
conditions:
- label="backport develop"

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

25
commitlint.config.js Normal file
View File

@ -0,0 +1,25 @@
module.exports = {
parserPreset: 'conventional-changelog-conventionalcommits',
rules: {
'subject-empty': [2, 'never'],
'type-case': [2, 'always', 'lower-case'],
'type-empty': [2, 'never'],
'type-enum': [
2,
'always',
[
'build',
'chore',
'ci',
'docs',
'feat',
'fix',
'perf',
'refactor',
'revert',
'style',
'test',
],
],
},
};

View File

@ -1,11 +0,0 @@
{
"baseUrl": "http://test_site:8000/",
"projectId": "da59y9",
"adminPassword": "admin",
"defaultCommandTimeout": 20000,
"pageLoadTimeout": 15000,
"retries": {
"runMode": 2,
"openMode": 2
}
}

View File

@ -1,5 +0,0 @@
{
"name": "Using fixtures to represent data",
"email": "hello@cypress.io",
"body": "Fixtures are a great way to mock data for responses to routes"
}

View File

@ -1,44 +0,0 @@
describe("Bulk Transaction Processing", () => {
before(() => {
cy.login();
cy.visit("/app/website");
});
it("Creates To Sales Order", () => {
cy.visit("/app/sales-order");
cy.url().should("include", "/sales-order");
cy.window()
.its("frappe.csrf_token")
.then((csrf_token) => {
return cy
.request({
url: "/api/method/erpnext.tests.ui_test_bulk_transaction_processing.create_records",
method: "POST",
headers: {
Accept: "application/json",
"Content-Type": "application/json",
"X-Frappe-CSRF-Token": csrf_token,
},
timeout: 60000,
})
.then((res) => {
expect(res.status).eq(200);
});
});
cy.wait(5000);
cy.get(
".list-row-head > .list-header-subject > .list-row-col > .list-check-all"
).check({ force: true });
cy.wait(3000);
cy.get(".actions-btn-group > .btn-primary").click({ force: true });
cy.wait(3000);
cy.get(".dropdown-menu-right > .user-action > .dropdown-item")
.contains("Sales Invoice")
.click({ force: true });
cy.wait(3000);
cy.get(".modal-content > .modal-footer > .standard-actions")
.contains("Yes")
.click({ force: true });
cy.contains("Creation of Sales Invoice successful");
});
});

View File

@ -1,13 +0,0 @@
context('Customer', () => {
before(() => {
cy.login();
});
it('Check Customer Group', () => {
cy.visit(`app/customer/`);
cy.get('.primary-action').click();
cy.wait(500);
cy.get('.custom-actions > .btn').click();
cy.get_field('customer_group', 'Link').should('have.value', 'All Customer Groups');
});
});

View File

@ -1,44 +0,0 @@
describe("Test Item Dashboard", () => {
before(() => {
cy.login();
cy.visit("/app/item");
cy.insert_doc(
"Item",
{
item_code: "e2e_test_item",
item_group: "All Item Groups",
opening_stock: 42,
valuation_rate: 100,
},
true
);
cy.go_to_doc("item", "e2e_test_item");
});
it("should show dashboard with correct data on first load", () => {
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
cy.get(".stock-levels").contains("e2e_test_item").should("exist");
// reserved and available qty
cy.get(".stock-levels .inline-graph-count")
.eq(0)
.contains("0")
.should("exist");
cy.get(".stock-levels .inline-graph-count")
.eq(1)
.contains("42")
.should("exist");
});
it("should persist on field change", () => {
cy.get('input[data-fieldname="disabled"]').check();
cy.wait(500);
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
cy.get(".stock-levels").should("have.length", 1);
});
it("should persist on reload", () => {
cy.reload();
cy.get(".stock-levels").contains("Stock Levels").should("be.visible");
});
});

View File

@ -1,116 +0,0 @@
context('Organizational Chart', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
it('navigates to org chart', () => {
cy.visit('/app');
cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
timeout: 60000
}).then(res => {
expect(res.status).eq(200);
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
.type('Test Org Chart{downarrow}{enter}', { force: true })
.blur({ force: true });
});
});
});
it('renders root nodes and loads children for the first expandable node', () => {
// check rendered root nodes and the node name, title, connections
cy.get('.hierarchy').find('.root-level ul.node-children').children()
.should('have.length', 2)
.first()
.as('first-child');
cy.get('@first-child').get('.node-name').contains('Test Employee 1');
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections');
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// children of 1st root visible
cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node');
cy.get('@child-node')
.should('have.length', 1)
.should('be.visible');
cy.get('@child-node').get('.node-name').contains('Test Employee 3');
// connectors between first root node and immediate child
cy.get(`path[data-parent="${employee_records.message[0]}"]`)
.should('be.visible')
.invoke('attr', 'data-child')
.should('equal', employee_records.message[2]);
});
});
it('hides active nodes children and connectors on expanding sibling node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// click sibling
cy.get(`#${employee_records.message[1]}`)
.click()
.should('have.class', 'active');
// child nodes and connectors hidden
cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
});
});
it('collapses previous level nodes and refreshes connectors on expanding child node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// click child node
cy.get(`#${employee_records.message[3]}`)
.click()
.should('have.class', 'active');
// previous level nodes: parent should be on active-path; other nodes should be collapsed
cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed');
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
// previous level connectors refreshed
cy.get(`path[data-parent="${employee_records.message[1]}"]`)
.should('have.class', 'collapsed-connector');
// child node's children and connectors rendered
cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible');
cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible');
});
});
it('expands previous level nodes', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`)
.click()
.should('have.class', 'active');
cy.get(`[data-parent="${employee_records.message[0]}"]`)
.should('be.visible');
cy.get('ul.hierarchy').children().should('have.length', 2);
cy.get(`#connectors`).children().should('have.length', 1);
});
});
it('edit node navigates to employee master', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
.click();
cy.url().should('include', `/employee/${employee_records.message[0]}`);
});
});
});

View File

@ -1,195 +0,0 @@
context('Organizational Chart Mobile', () => {
before(() => {
cy.login();
cy.visit('/app/website');
});
it('navigates to org chart', () => {
cy.viewport(375, 667);
cy.visit('/app');
cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {
return cy.request({
url: `/api/method/erpnext.tests.ui_test_helpers.create_employee_records`,
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
'X-Frappe-CSRF-Token': csrf_token
},
timeout: 60000
}).then(res => {
expect(res.status).eq(200);
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
.type('Test Org Chart{downarrow}{enter}', { force: true })
.blur({ force: true });
});
});
});
it('renders root nodes', () => {
// check rendered root nodes and the node name, title, connections
cy.get('.hierarchy-mobile').find('.root-level').children()
.should('have.length', 2)
.first()
.as('first-child');
cy.get('@first-child').get('.node-name').contains('Test Employee 1');
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2');
});
it('expands root node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[1]}`)
.click()
.should('have.class', 'active');
// other root node removed
cy.get(`#${employee_records.message[0]}`).should('not.exist');
// children of active root node
cy.get('.hierarchy-mobile').find('.level').first().find('ul.node-children').children()
.should('have.length', 2);
cy.get(`div[data-parent="${employee_records.message[1]}"]`).first().as('child-node');
cy.get('@child-node').should('be.visible');
cy.get('@child-node')
.get('.node-name')
.contains('Test Employee 4');
// connectors between root node and immediate children
cy.get(`path[data-parent="${employee_records.message[1]}"]`).as('connectors');
cy.get('@connectors')
.should('have.length', 2)
.should('be.visible');
cy.get('@connectors')
.first()
.invoke('attr', 'data-child')
.should('eq', employee_records.message[3]);
});
});
it('expands child node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[3]}`)
.click()
.should('have.class', 'active')
.as('expanded_node');
// 2 levels on screen; 1 on active path; 1 collapsed
cy.get('.hierarchy-mobile').children().should('have.length', 2);
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
// children of expanded node visible
cy.get('@expanded_node')
.next()
.should('have.class', 'node-children')
.as('node-children');
cy.get('@node-children').children().should('have.length', 1);
cy.get('@node-children')
.first()
.get('.node-card')
.should('have.class', 'active-child')
.contains('Test Employee 7');
// orphan connectors removed
cy.get(`#connectors`).children().should('have.length', 2);
});
});
it('renders sibling group', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// sibling group visible for parent
cy.get(`#${employee_records.message[1]}`)
.next()
.as('sibling_group');
cy.get('@sibling_group')
.should('have.attr', 'data-parent', 'undefined')
.should('have.class', 'node-group')
.and('have.class', 'collapsed');
cy.get('@sibling_group').get('.avatar-group').children().as('siblings');
cy.get('@siblings').should('have.length', 1);
cy.get('@siblings')
.first()
.should('have.attr', 'title', 'Test Employee 1');
});
});
it('expands previous level nodes', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[6]}`)
.click()
.should('have.class', 'active');
// clicking on previous level node should remove all the nodes ahead
// and expand that node
cy.get(`#${employee_records.message[3]}`).click();
cy.get(`#${employee_records.message[3]}`)
.should('have.class', 'active')
.should('not.have.class', 'active-path');
cy.get(`#${employee_records.message[6]}`).should('have.class', 'active-child');
cy.get('.hierarchy-mobile').children().should('have.length', 2);
cy.get(`#connectors`).children().should('have.length', 2);
});
});
it('expands sibling group', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// sibling group visible for parent
cy.get(`#${employee_records.message[6]}`).click();
cy.get(`#${employee_records.message[3]}`)
.next()
.click();
// siblings of parent should be visible
cy.get('.hierarchy-mobile').prev().as('sibling_group');
cy.get('@sibling_group')
.should('exist')
.should('have.class', 'sibling-group')
.should('not.have.class', 'collapsed');
cy.get(`#${employee_records.message[1]}`)
.should('be.visible')
.should('have.class', 'active');
cy.get(`[data-parent="${employee_records.message[1]}"]`)
.should('be.visible')
.should('have.length', 2)
.should('have.class', 'active-child');
});
});
it('goes to the respective level after clicking on non-collapsed sibling group', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(() => {
// click on non-collapsed sibling group
cy.get('.hierarchy-mobile')
.prev()
.click();
// should take you to that level
cy.get('.hierarchy-mobile').find('li.level .node-card').should('have.length', 2);
});
});
it('edit node navigates to employee master', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
.click();
cy.url().should('include', `/employee/${employee_records.message[0]}`);
});
});
});

View File

@ -1,17 +0,0 @@
// ***********************************************************
// This example plugins/index.js can be used to load plugins
//
// You can change the location of this file or turn off loading
// the plugins file with the 'pluginsFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/plugins-guide
// ***********************************************************
// This function is called when a project is opened or re-opened (e.g. due to
// the project's config changing)
module.exports = () => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
};

View File

@ -1,31 +0,0 @@
// ***********************************************
// This example commands.js shows you how to
// create various custom commands and overwrite
// existing commands.
//
// For more comprehensive examples of custom
// commands please read more here:
// https://on.cypress.io/custom-commands
// ***********************************************
//
//
// -- This is a parent command --
// Cypress.Commands.add("login", (email, password) => { ... });
//
//
// -- This is a child command --
// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... });
//
//
// -- This is a dual command --
// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... });
//
//
// -- This is will overwrite an existing command --
// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... });
const slug = (name) => name.toLowerCase().replace(" ", "-");
Cypress.Commands.add("go_to_doc", (doctype, name) => {
cy.visit(`/app/${slug(doctype)}/${encodeURIComponent(name)}`);
});

View File

@ -1,26 +0,0 @@
// ***********************************************************
// This example support/index.js is processed and
// loaded automatically before your test files.
//
// This is a great place to put global configuration and
// behavior that modifies Cypress.
//
// You can change the location of this file or turn off
// automatically serving support files with the
// 'supportFile' configuration option.
//
// You can read more here:
// https://on.cypress.io/configuration
// ***********************************************************
// Import commands.js using ES2015 syntax:
import './commands';
import '../../../frappe/cypress/support/commands' // eslint-disable-line
// Alternatively you can use CommonJS syntax:
// require('./commands')
Cypress.Cookies.defaults({
preserve: 'sid'
});

View File

@ -1,12 +0,0 @@
{
"compilerOptions": {
"allowJs": true,
"baseUrl": "../node_modules",
"types": [
"cypress"
]
},
"include": [
"**/*.*"
]
}

View File

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

View File

@ -10,4 +10,42 @@ Entries are:
- Sales Invoice (Itemised)
- Purchase Invoice (Itemised)
All accounting entries are stored in the `General Ledger`
All accounting entries are stored in the `General Ledger`
## Payment Ledger
Transactions on Receivable and Payable Account types will also be stored in `Payment Ledger`. This is so that payment reconciliation process only requires update on this ledger.
### Key Fields
| Field | Description |
|----------------------|----------------------------------|
| `account_type` | Receivable/Payable |
| `account` | Accounting head |
| `party` | Party Name |
| `voucher_no` | Voucher No |
| `against_voucher_no` | Linked voucher(secondary effect) |
| `amount` | can be +ve/-ve |
### Design
`debit` and `credit` have been replaced with `account_type` and `amount`. `against_voucher_no` is populated for all entries. So, outstanding amount can be calculated by summing up amount only using `against_voucher_no`.
Ex:
1. Consider an invoice for ₹100 and a partial payment of ₹80 against that invoice. Payment Ledger will have following entries.
| voucher_no | against_voucher_no | amount |
|------------|--------------------|--------|
| SINV-01 | SINV-01 | 100 |
| PAY-01 | SINV-01 | -80 |
2. Reconcile a Credit Note against an invoice using a Journal Entry
An invoice for ₹100 partially reconciled against a credit of ₹70 using a Journal Entry. Payment Ledger will have the following entries.
| voucher_no | against_voucher_no | amount |
|------------|--------------------|--------|
| SINV-01 | SINV-01 | 100 |
| | | |
| CR-NOTE-01 | CR-NOTE-01 | -70 |
| | | |
| JE-01 | CR-NOTE-01 | +70 |
| JE-01 | SINV-01 | -70 |

View File

@ -322,9 +322,9 @@ def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
"""select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s
and %s like %s order by name limit %s, %s"""
and %s like %s order by name limit %s offset %s"""
% ("%s", searchfield, "%s", "%s", "%s"),
(filters["company"], "%%%s%%" % txt, start, page_len),
(filters["company"], "%%%s%%" % txt, page_len, start),
as_list=1,
)

View File

@ -234,17 +234,19 @@ def get_checks_for_pl_and_bs_accounts():
return dimensions
def get_dimension_with_children(doctype, dimension):
def get_dimension_with_children(doctype, dimensions):
if isinstance(dimension, list):
dimension = dimension[0]
if isinstance(dimensions, str):
dimensions = [dimensions]
all_dimensions = []
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children]
for dimension in dimensions:
lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
children = frappe.get_all(
doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
)
all_dimensions += [c.name for c in children]
return all_dimensions

View File

@ -49,15 +49,9 @@ class AccountingPeriod(Document):
@frappe.whitelist()
def get_doctypes_for_closing(self):
docs_for_closing = []
doctypes = [
"Sales Invoice",
"Purchase Invoice",
"Journal Entry",
"Payroll Entry",
"Bank Clearance",
"Asset",
"Stock Entry",
]
# get period closing doctypes from all the apps
doctypes = frappe.get_hooks("period_closing_doctypes")
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
for closed_doctype in closed_doctypes:
docs_for_closing.append(closed_doctype)

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

@ -104,7 +104,7 @@ class BankClearance(Document):
loan_repayment = frappe.qb.DocType("Loan Repayment")
loan_repayments = (
query = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document"),
@ -118,13 +118,17 @@ class BankClearance(Document):
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
.orderby(loan_repayment.posting_date)
.orderby(loan_repayment.name, frappe.qb.desc)
).run(as_dict=1)
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
query = query.orderby(loan_repayment.posting_date).orderby(loan_repayment.name, frappe.qb.desc)
loan_repayments = query.run(as_dict=True)
pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions:

View File

@ -10,7 +10,6 @@ from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_company_currency
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system,
@ -368,6 +367,27 @@ def get_queries(bank_account, company, transaction, document_types):
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
queries = []
# get matching queries from all the apps
for method_name in frappe.get_hooks("get_matching_queries"):
queries.extend(
frappe.get_attr(method_name)(
bank_account,
company,
transaction,
document_types,
amount_condition,
account_from_to,
)
or []
)
return queries
def get_matching_queries(
bank_account, company, transaction, document_types, amount_condition, account_from_to
):
queries = []
if "payment_entry" in document_types:
pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
queries.extend([pe_amount_matching])
@ -385,10 +405,6 @@ def get_queries(bank_account, company, transaction, document_types):
pi_amount_matching = get_pi_matching_query(amount_condition)
queries.extend([pi_amount_matching])
if "expense_claim" in document_types:
ec_amount_matching = get_ec_matching_query(bank_account, company, amount_condition)
queries.extend([ec_amount_matching])
return queries
@ -467,11 +483,13 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.repay_from_salary == 0)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account)
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if amount_condition:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
@ -602,37 +620,3 @@ def get_pi_matching_query(amount_condition):
AND ifnull(clearance_date, '') = ""
AND cash_bank_account = %(bank_account)s
"""
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
mode_of_payments = [
x["parent"]
for x in frappe.db.get_all(
"Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"]
)
]
mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )"
company_currency = get_company_currency(company)
return f"""
SELECT
( CASE WHEN employee = %(party)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Expense Claim' as doctype,
name,
total_sanctioned_amount as paid_amount,
'' as reference_no,
'' as reference_date,
employee as party,
'Employee' as party_type,
posting_date,
'{company_currency}' as currency
FROM
`tabExpense Claim`
WHERE
total_sanctioned_amount {amount_condition} %(amount)s
AND docstatus = 1
AND is_paid = 1
AND ifnull(clearance_date, '') = ""
AND mode_of_payment in {mode_of_payments}
"""

View File

@ -3,28 +3,21 @@
frappe.ui.form.on("Bank Transaction", {
onload(frm) {
frm.set_query("payment_document", "payment_entries", function () {
frm.set_query("payment_document", "payment_entries", function() {
const payment_doctypes = frm.events.get_payment_doctypes(frm);
return {
filters: {
name: [
"in",
[
"Payment Entry",
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
"Expense Claim",
],
],
name: ["in", payment_doctypes],
},
};
});
},
bank_account: function (frm) {
bank_account: function(frm) {
set_bank_statement_filter(frm);
},
setup: function (frm) {
setup: function(frm) {
frm.set_query("party_type", function () {
return {
filters: {
@ -33,6 +26,16 @@ frappe.ui.form.on("Bank Transaction", {
};
});
},
get_payment_doctypes: function() {
// get payment doctypes from all the apps
return [
"Payment Entry",
"Journal Entry",
"Sales Invoice",
"Purchase Invoice",
];
}
});
frappe.ui.form.on("Bank Transaction Payments", {

View File

@ -58,18 +58,10 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
if payment_entry.payment_document in [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Expense Claim",
"Loan Repayment",
"Loan Disbursement",
]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
if payment_entry.payment_document == "Sales Invoice":
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry":
@ -95,6 +87,12 @@ class BankTransaction(StatusUpdater):
)
@frappe.whitelist()
def get_doctypes_for_bank_reconciliation():
"""Get Bank Reconciliation doctypes from all the apps"""
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all(
"Bank Transaction Payments",

View File

@ -5,6 +5,7 @@ import json
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
get_linked_payments,
@ -18,28 +19,21 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
@classmethod
def setUpClass(cls):
class TestBankTransaction(FrappeTestCase):
def setUp(self):
for dt in [
"Loan Repayment",
"Bank Transaction",
"Payment Entry",
"Payment Entry Reference",
"POS Profile",
]:
frappe.db.delete(dt)
make_pos_profile()
add_transactions()
add_vouchers()
@classmethod
def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
if doc.docstatus == 1:
doc.cancel()
doc.delete()
# Delete directly in DB to avoid validation errors for countries not allowing deletion
frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""")
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
bank_transaction = frappe.get_doc(
@ -155,6 +149,35 @@ class TestBankTransaction(unittest.TestCase):
is not None
)
def test_matching_loan_repayment(self):
from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts
create_loan_accounts()
bank_account = frappe.get_doc(
{
"doctype": "Bank Account",
"account_name": "Payment Account",
"bank": "Citi Bank",
"account": "Payment Account - _TC",
}
).insert(ignore_if_duplicate=True)
bank_transaction = frappe.get_doc(
{
"doctype": "Bank Transaction",
"description": "Loan Repayment - OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": "2018-10-27",
"deposit": 500,
"currency": "INR",
"bank_account": bank_account.name,
}
).submit()
repayment_entry = create_loan_and_repayment()
linked_payments = get_linked_payments(bank_transaction.name, ["loan_repayment", "exact_match"])
self.assertEqual(linked_payments[0][2], repayment_entry.name)
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
@ -364,3 +387,59 @@ def add_vouchers():
)
si.insert()
si.submit()
def create_loan_and_repayment():
from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_term_loans,
)
from erpnext.setup.doctype.employee.test_employee import make_employee
create_loan_type(
"Personal Loan",
500000,
8.4,
is_term_loan=1,
mode_of_payment="Cash",
disbursement_account="Disbursement Account - _TC",
payment_account="Payment Account - _TC",
loan_account="Loan Account - _TC",
interest_income_account="Interest Income Account - _TC",
penalty_income_account="Penalty Income Account - _TC",
)
applicant = make_employee("test_bank_reco@loan.com", company="_Test Company")
loan = create_loan(applicant, "Personal Loan", 5000, "Repay Over Number of Periods", 20)
loan = frappe.get_doc(
{
"doctype": "Loan",
"applicant_type": "Employee",
"company": "_Test Company",
"applicant": applicant,
"loan_type": "Personal Loan",
"loan_amount": 5000,
"repayment_method": "Repay Fixed Amount per Period",
"monthly_repayment_amount": 500,
"repayment_start_date": "2018-09-27",
"is_term_loan": 1,
"posting_date": "2018-09-27",
}
).insert()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date="2018-09-27")
process_loan_interest_accrual_for_term_loans(posting_date="2018-10-27")
repayment_entry = create_repayment_entry(
loan.name,
applicant,
"2018-10-27",
500,
)
repayment_entry.submit()
return repayment_entry

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

@ -42,7 +42,7 @@ class GLEntry(Document):
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
if not self.flags.from_repost:
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account()
@ -51,23 +51,27 @@ class GLEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
# Update outstanding amt on against voucher
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.against_voucher
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_outstanding_amt(
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
)
if frappe.db.get_value("Account", self.account, "account_type") not in [
"Receivable",
"Payable",
]:
# Update outstanding amt on against voucher
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.against_voucher
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_outstanding_amt(
self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
)
def check_mandatory(self):
mandatory = ["account", "voucher_type", "voucher_no", "company"]

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");
}
});
}
}
},
@ -240,25 +224,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
me.frm.set_query("reference_name", "accounts", function(doc, cdt, cdn) {
var jvd = frappe.get_doc(cdt, cdn);
// expense claim
if(jvd.reference_type==="Expense Claim") {
return {
filters: {
'total_sanctioned_amount': ['>', 0],
'status': ['!=', 'Paid'],
'docstatus': 1
}
};
}
if(jvd.reference_type==="Employee Advance") {
return {
filters: {
'docstatus': 1
}
};
}
// journal entry
if(jvd.reference_type==="Journal Entry") {
frappe.model.validate_missing(jvd, "account");
@ -271,13 +236,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
};
}
// payroll entry
if(jvd.reference_type==="Payroll Entry") {
return {
query: "erpnext.payroll.doctype.payroll_entry.payroll_entry.get_payroll_entries_for_jv",
};
}
var out = {
filters: [
[jvd.reference_type, "docstatus", "=", 1]

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

@ -24,7 +24,6 @@ from erpnext.accounts.utils import (
get_stock_and_account_balance,
)
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
class StockAccountInvalidTransaction(frappe.ValidationError):
@ -66,7 +65,6 @@ class JournalEntry(AccountsController):
self.set_against_account()
self.create_remarks()
self.set_print_format_fields()
self.validate_expense_claim()
self.validate_credit_debit_note()
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
@ -83,27 +81,21 @@ class JournalEntry(AccountsController):
self.check_credit_limit()
self.make_gl_entries()
self.update_advance_paid()
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
self.update_status_for_full_and_final_statement()
def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
from erpnext.payroll.doctype.salary_slip.salary_slip import unlink_ref_doc_from_salary_slip
unlink_ref_doc_from_payment_entries(self)
unlink_ref_doc_from_salary_slip(self.name)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
self.unlink_advance_entry_reference()
self.unlink_asset_reference()
self.unlink_inter_company_jv()
self.unlink_asset_adjustment_entry()
self.update_invoice_discounting()
self.update_status_for_full_and_final_statement()
def get_title(self):
return self.pay_to_recd_from or self.accounts[0].account
@ -112,21 +104,13 @@ class JournalEntry(AccountsController):
advance_paid = frappe._dict()
for d in self.get("accounts"):
if d.is_advance:
if d.reference_type in ("Sales Order", "Purchase Order", "Employee Advance"):
if d.reference_type in frappe.get_hooks("advance_payment_doctypes"):
advance_paid.setdefault(d.reference_type, []).append(d.reference_name)
for voucher_type, order_list in advance_paid.items():
for voucher_no in list(set(order_list)):
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
def update_status_for_full_and_final_statement(self):
for entry in self.accounts:
if entry.reference_type == "Full and Final Statement":
if self.docstatus == 1:
frappe.db.set_value("Full and Final Statement", entry.reference_name, "status", "Paid")
elif self.docstatus == 2:
frappe.db.set_value("Full and Final Statement", entry.reference_name, "status", "Unpaid")
def validate_inter_company_accounts(self):
if (
self.voucher_type == "Inter Company Journal Entry"
@ -416,7 +400,7 @@ class JournalEntry(AccountsController):
against_entries = frappe.db.sql(
"""select * from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
and (reference_type is null or reference_type in ('', 'Sales Order', 'Purchase Order'))
""",
(d.account, d.reference_name),
as_dict=True,
@ -800,9 +784,7 @@ class JournalEntry(AccountsController):
self.total_amount_in_words = money_in_words(amt, currency)
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
def build_gl_map(self):
gl_map = []
for d in self.get("accounts"):
if d.debit or d.credit:
@ -838,7 +820,12 @@ class JournalEntry(AccountsController):
item=d,
)
)
return gl_map
def make_gl_entries(self, cancel=0, adv_adj=0):
from erpnext.accounts.general_ledger import make_gl_entries
gl_map = self.build_gl_map()
if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
update_outstanding = "No"
else:
@ -932,29 +919,6 @@ class JournalEntry(AccountsController):
as_dict=True,
)
def update_expense_claim(self):
for d in self.accounts:
if d.reference_type == "Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.debit)
else:
update_reimbursed_amount(doc, d.debit)
def validate_expense_claim(self):
for d in self.accounts:
if d.reference_type == "Expense Claim":
sanctioned_amount, reimbursed_amount = frappe.db.get_value(
"Expense Claim", d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed")
)
pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
if d.debit > pending_amount:
frappe.throw(
_(
"Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}"
).format(d.idx, d.reference_name, pending_amount)
)
def validate_credit_debit_note(self):
if self.stock_entry:
if frappe.db.get_value("Stock Entry", self.stock_entry, "docstatus") != 1:
@ -1201,24 +1165,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):
@ -1239,7 +1185,7 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
AND jv.docstatus = 1
AND jv.`{0}` LIKE %(txt)s
ORDER BY jv.name DESC
LIMIT %(offset)s, %(limit)s
LIMIT %(limit)s offset %(offset)s
""".format(
searchfield
),

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

@ -110,8 +110,6 @@ frappe.ui.form.on('Payment Entry', {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
} else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
} else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"];
} else {
var doctypes = ["Journal Entry"];
}
@ -140,17 +138,12 @@ frappe.ui.form.on('Payment Entry', {
const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
'Purchase Order', 'Dunning'];
if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party;
}
if(child.reference_doctype == "Expense Claim") {
filters["docstatus"] = 1;
filters["is_paid"] = 0;
}
return {
filters: filters
};
@ -730,7 +723,7 @@ frappe.ui.form.on('Payment Entry', {
c.payment_term = d.payment_term;
c.allocated_amount = d.allocated_amount;
if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) {
if(!in_list(frm.events.get_order_doctypes(frm), d.voucher_type)) {
if(flt(d.outstanding_amount) > 0)
total_positive_outstanding += flt(d.outstanding_amount);
else
@ -745,7 +738,7 @@ frappe.ui.form.on('Payment Entry', {
} else {
c.exchange_rate = 1;
}
if (in_list(['Sales Invoice', 'Purchase Invoice', "Expense Claim", "Fees"], d.reference_doctype)){
if (in_list(frm.events.get_invoice_doctypes(frm), d.reference_doctype)){
c.due_date = d.due_date;
}
});
@ -776,6 +769,14 @@ frappe.ui.form.on('Payment Entry', {
});
},
get_order_doctypes: function(frm) {
return ["Sales Order", "Purchase Order"];
},
get_invoice_doctypes: function(frm) {
return ["Sales Invoice", "Purchase Invoice"];
},
allocate_party_amount_against_ref_docs: function(frm, paid_amount, paid_amount_change) {
var total_positive_outstanding_including_order = 0;
var total_negative_outstanding = 0;
@ -946,14 +947,6 @@ frappe.ui.form.on('Payment Entry', {
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Purchase Order, Purchase Invoice or Journal Entry", [row.idx]));
return false;
}
if(frm.doc.party_type=="Employee" &&
!in_list(["Expense Claim", "Journal Entry"], row.reference_doctype)
) {
frappe.model.set_value(row.doctype, row.name, "against_voucher_type", null);
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false;
}
}
if (row) {

View File

@ -6,7 +6,7 @@ import json
from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
from frappe import ValidationError, _, qb, scrub, throw
from frappe.utils import cint, comma_or, flt, getdate, nowdate
import erpnext
@ -29,7 +29,6 @@ from erpnext.controllers.accounts_controller import (
get_supplier_block_status,
validate_taxes_and_charges,
)
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
from erpnext.setup.utils import get_exchange_rate
@ -88,16 +87,14 @@ class PaymentEntry(AccountsController):
if self.difference_amount:
frappe.throw(_("Difference Amount must be zero"))
self.make_gl_entries()
self.update_expense_claim()
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_payment_schedule()
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.make_gl_entries(cancel=1)
self.update_expense_claim()
self.update_outstanding_amounts()
self.update_advance_paid()
self.delink_advance_entry_references()
@ -296,14 +293,7 @@ class PaymentEntry(AccountsController):
frappe.throw(_("{0} is mandatory").format(self.meta.get_label(field)))
def validate_reference_documents(self):
if self.party_type == "Customer":
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
elif self.party_type == "Employee":
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder":
valid_reference_doctypes = "Journal Entry"
valid_reference_doctypes = self.get_valid_reference_doctypes()
for d in self.get("references"):
if not d.allocated_amount:
@ -329,7 +319,7 @@ class PaymentEntry(AccountsController):
else:
self.validate_journal_entry()
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Expense Claim", "Fees"):
if d.reference_doctype in frappe.get_hooks("invoice_doctypes"):
if self.party_type == "Customer":
ref_party_account = (
get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
@ -346,9 +336,25 @@ class PaymentEntry(AccountsController):
)
)
if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
frappe.throw(
_("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
title=_("Invalid Invoice"),
)
if ref_doc.docstatus != 1:
frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
def get_valid_reference_doctypes(self):
if self.party_type == "Customer":
return ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier":
return ("Purchase Order", "Purchase Invoice", "Journal Entry")
elif self.party_type == "Shareholder":
return ("Journal Entry",)
elif self.party_type == "Employee":
return ("Journal Entry",)
def validate_paid_invoices(self):
no_oustanding_refs = {}
@ -779,7 +785,7 @@ class PaymentEntry(AccountsController):
self.set("remarks", "\n".join(remarks))
def make_gl_entries(self, cancel=0, adv_adj=0):
def build_gl_map(self):
if self.payment_type in ("Receive", "Pay") and not self.get("party_account_field"):
self.setup_party_account_field()
@ -788,7 +794,10 @@ class PaymentEntry(AccountsController):
self.add_bank_gl_entries(gl_entries)
self.add_deductions_gl_entries(gl_entries)
self.add_tax_gl_entries(gl_entries)
return gl_entries
def make_gl_entries(self, cancel=0, adv_adj=0):
gl_entries = self.build_gl_map()
gl_entries = process_gl_map(gl_entries)
make_gl_entries(gl_entries, cancel=cancel, adv_adj=adv_adj)
@ -971,24 +980,9 @@ class PaymentEntry(AccountsController):
def update_advance_paid(self):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in (
"Sales Order",
"Purchase Order",
"Employee Advance",
"Gratuity",
):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
def update_expense_claim(self):
if self.payment_type in ("Pay") and self.party:
for d in self.get("references"):
if d.reference_doctype == "Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.allocated_amount)
else:
update_reimbursed_amount(doc, d.allocated_amount)
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
self.reference_date = nowdate()
@ -1182,13 +1176,15 @@ def validate_inclusive_tax(tax, doc):
@frappe.whitelist()
def get_outstanding_reference_documents(args):
if isinstance(args, str):
args = json.loads(args)
if args.get("party_type") == "Member":
return
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
# confirm that Supplier is not blocked
if args.get("party_type") == "Supplier":
supplier_status = get_supplier_block_status(args["party"])
@ -1210,10 +1206,13 @@ def get_outstanding_reference_documents(args):
condition = " and voucher_type={0} and voucher_no={1}".format(
frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
)
common_filter.append(ple.voucher_type == args["voucher_type"])
common_filter.append(ple.voucher_no == args["voucher_no"])
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
common_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
@ -1225,16 +1224,19 @@ def get_outstanding_reference_documents(args):
condition += " and {0} between '{1}' and '{2}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
common_filter.append(ple.company == args.get("company"))
outstanding_invoices = get_outstanding_invoices(
args.get("party_type"),
args.get("party"),
args.get("party_account"),
filters=args,
condition=condition,
common_filter=common_filter,
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@ -1242,7 +1244,7 @@ def get_outstanding_reference_documents(args):
for d in outstanding_invoices:
d["exchange_rate"] = 1
if party_account_currency != company_currency:
if d.voucher_type in ("Sales Invoice", "Purchase Invoice", "Expense Claim"):
if d.voucher_type in frappe.get_hooks("invoice_doctypes"):
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
elif d.voucher_type == "Journal Entry":
d["exchange_rate"] = get_exchange_rate(
@ -1438,7 +1440,7 @@ def get_negative_outstanding_invoices(
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = ""
if voucher_type == "Purchase Invoice":
supplier_condition = "and (release_date is null or release_date <= CURDATE())"
supplier_condition = "and (release_date is null or release_date <= CURRENT_DATE)"
if party_account_currency == company_currency:
grand_total_field = "base_grand_total"
rounded_total_field = "base_rounded_total"
@ -1569,20 +1571,17 @@ def get_outstanding_on_journal_entry(name):
@frappe.whitelist()
def get_reference_details(reference_doctype, reference_name, party_account_currency):
total_amount = outstanding_amount = exchange_rate = bill_no = None
total_amount = outstanding_amount = exchange_rate = None
ref_doc = frappe.get_doc(reference_doctype, reference_name)
company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(
ref_doc.company
)
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
if reference_doctype == "Dunning":
total_amount = outstanding_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
outstanding_amount = ref_doc.get("dunning_amount")
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
@ -1592,16 +1591,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
elif reference_doctype != "Journal Entry":
if ref_doc.doctype == "Expense Claim":
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
elif ref_doc.doctype == "Gratuity":
total_amount = ref_doc.amount
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@ -1614,26 +1605,12 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate(
party_account_currency, company_currency, ref_doc.posting_date
)
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
outstanding_amount = (
flt(ref_doc.get("total_sanctioned_amount"))
+ flt(ref_doc.get("total_taxes_and_charges"))
- flt(ref_doc.get("total_amount_reimbursed"))
- flt(ref_doc.get("total_advance_amount"))
)
elif reference_doctype == "Employee Advance":
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
elif reference_doctype == "Gratuity":
outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount)
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
# Get the exchange rate based on the posting date of the ref doc.
exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
@ -1644,114 +1621,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
"total_amount": flt(total_amount),
"outstanding_amount": flt(outstanding_amount),
"exchange_rate": flt(exchange_rate),
"bill_no": bill_no,
"bill_no": ref_doc.get("bill_no"),
}
)
def get_amounts_based_on_reference_doctype(
reference_doctype, ref_doc, party_account_currency, company_currency, reference_name
):
total_amount = outstanding_amount = exchange_rate = None
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
outstanding_amount = ref_doc.get("dunning_amount")
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
exchange_rate = get_exchange_rate(
party_account_currency, company_currency, ref_doc.posting_date
)
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
return total_amount, outstanding_amount, exchange_rate
def get_amounts_based_on_ref_doc(
reference_doctype, ref_doc, party_account_currency, company_currency
):
total_amount = outstanding_amount = exchange_rate = None
if ref_doc.doctype == "Expense Claim":
total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(
party_account_currency, ref_doc
)
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc
)
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc
exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate(
party_account_currency, company_currency, ref_doc.posting_date
)
outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts(
reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency
)
return total_amount, outstanding_amount, exchange_rate, bill_no
def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc):
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
return total_amount, exchange_rate
def get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc
):
exchange_rate = None
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
return total_amount, exchange_rate
def get_bill_no_and_update_amounts(
reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency
):
outstanding_amount = bill_no = None
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
outstanding_amount = (
flt(ref_doc.get("total_sanctioned_amount"))
+ flt(ref_doc.get("total_taxes_and_charges"))
- flt(ref_doc.get("total_amount_reimbursed"))
- flt(ref_doc.get("total_advance_amount"))
)
elif reference_doctype == "Employee Advance":
outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
return outstanding_amount, exchange_rate, bill_no
@frappe.whitelist()
def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None):
reference_doc = None
@ -1870,8 +1744,6 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.set_missing_values()
if party_account and bank:
if dt == "Employee Advance":
reference_doc = doc
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
if discount_amount:
@ -1906,8 +1778,6 @@ def set_party_type(dt):
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee"
return party_type
@ -1918,12 +1788,6 @@ def set_party_account(dt, dn, doc, party_type):
party_account = doc.credit_to
elif dt == "Fees":
party_account = doc.receivable_account
elif dt == "Employee Advance":
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
elif dt == "Gratuity":
party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
@ -1958,24 +1822,12 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
else:
grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
outstanding_amount = doc.grand_total - doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = flt(doc.advance_amount)
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
if party_account_currency != doc.currency:
grand_total = flt(doc.advance_amount) * flt(doc.exchange_rate)
outstanding_amount = (flt(doc.advance_amount) - flt(doc.paid_amount)) * flt(doc.exchange_rate)
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
elif dt == "Gratuity":
grand_total = doc.amount
outstanding_amount = flt(doc.amount) - flt(doc.paid_amount)
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
@ -1997,8 +1849,6 @@ def set_paid_amount_and_received_amount(
received_amount = bank_amount
else:
received_amount = paid_amount * doc.get("conversion_rate", 1)
if dt == "Employee Advance":
received_amount = paid_amount * doc.get("exchange_rate", 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
@ -2006,8 +1856,6 @@ def set_paid_amount_and_received_amount(
else:
# if party account currency and bank currency is different then populate paid amount as well
paid_amount = received_amount * doc.get("conversion_rate", 1)
if dt == "Employee Advance":
paid_amount = received_amount * doc.get("exchange_rate", 1)
return paid_amount, received_amount

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

@ -4,6 +4,7 @@
import unittest
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@ -18,13 +19,16 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice,
create_sales_invoice_against_cost_center,
)
from erpnext.hr.doctype.expense_claim.test_expense_claim import make_expense_claim
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.setup.doctype.employee.test_employee import make_employee
test_dependencies = ["Item"]
class TestPaymentEntry(unittest.TestCase):
class TestPaymentEntry(FrappeTestCase):
def tearDown(self):
frappe.db.rollback()
def test_payment_entry_against_order(self):
so = make_sales_order()
pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Cash - _TC")
@ -293,31 +297,6 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(outstanding_amount), 250)
self.assertEqual(status, "Unpaid")
def test_payment_entry_against_ec(self):
payable = frappe.get_cached_value("Company", "_Test Company", "default_payable_account")
ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC")
pe = get_payment_entry(
"Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300
)
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 1
pe.paid_to = payable
pe.insert()
pe.submit()
expected_gle = dict(
(d[0], d) for d in [[payable, 300, 0, ec.name], ["_Test Bank USD - _TC", 0, 300, None]]
)
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(
frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")
) - flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed"))
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_against_si_usd_to_inr(self):
si = create_sales_invoice(
customer="_Test Customer USD",
@ -743,6 +722,25 @@ class TestPaymentEntry(unittest.TestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
# block invoice after creating payment entry
# since `get_payment_entry` will not attach blocked invoice to payment
pi.block_invoice()
with self.assertRaises(frappe.ValidationError) as err:
pe.save()
self.assertTrue("is on hold" in str(err.exception).lower())
def test_payment_entry_for_employee(self):
employee = make_employee("test_payment_entry@salary.com", company="_Test Company")
create_payment_entry(party_type="Employee", party=employee, save=True)
def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry")

View File

@ -0,0 +1,8 @@
// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Payment Ledger Entry', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,182 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2022-05-09 19:35:03.334361",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"posting_date",
"company",
"account_type",
"account",
"party_type",
"party",
"due_date",
"cost_center",
"finance_book",
"voucher_type",
"voucher_no",
"against_voucher_type",
"against_voucher_no",
"amount",
"account_currency",
"amount_in_account_currency",
"delinked"
],
"fields": [
{
"fieldname": "posting_date",
"fieldtype": "Date",
"label": "Posting Date"
},
{
"fieldname": "account_type",
"fieldtype": "Select",
"label": "Account Type",
"options": "Receivable\nPayable"
},
{
"fieldname": "account",
"fieldtype": "Link",
"label": "Account",
"options": "Account",
"search_index": 1
},
{
"fieldname": "party_type",
"fieldtype": "Link",
"label": "Party Type",
"options": "DocType",
"search_index": 1
},
{
"fieldname": "party",
"fieldtype": "Dynamic Link",
"label": "Party",
"options": "party_type",
"search_index": 1
},
{
"fieldname": "voucher_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Voucher Type",
"options": "DocType"
},
{
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"options": "voucher_type"
},
{
"fieldname": "against_voucher_type",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Against Voucher Type",
"options": "DocType"
},
{
"fieldname": "against_voucher_no",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Against Voucher No",
"options": "against_voucher_type"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "Company:company:default_currency"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "amount_in_account_currency",
"fieldtype": "Currency",
"label": "Amount in Account Currency",
"options": "account_currency"
},
{
"default": "0",
"fieldname": "delinked",
"fieldtype": "Check",
"in_list_view": 1,
"label": "DeLinked"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"search_index": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date"
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-07-11 09:13:54.379168",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Ledger Entry",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Auditor",
"share": 1
}
],
"search_fields": "voucher_no, against_voucher_no",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,154 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_checks_for_pl_and_bs_accounts,
)
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
from erpnext.accounts.doctype.gl_entry.gl_entry import (
validate_balance_type,
validate_frozen_account,
)
from erpnext.accounts.utils import update_voucher_outstanding
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
class PaymentLedgerEntry(Document):
def validate_account(self):
valid_account = frappe.db.get_list(
"Account",
"name",
filters={"name": self.account, "account_type": self.account_type, "company": self.company},
ignore_permissions=True,
)
if not valid_account:
frappe.throw(_("{0} account is not of type {1}").format(self.account, self.account_type))
def validate_account_details(self):
"""Account must be ledger, active and not freezed"""
ret = frappe.db.sql(
"""select is_group, docstatus, company
from tabAccount where name=%s""",
self.account,
as_dict=1,
)[0]
if ret.is_group == 1:
frappe.throw(
_(
"""{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
).format(self.voucher_type, self.voucher_no, self.account)
)
if ret.docstatus == 2:
frappe.throw(
_("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
)
if ret.company != self.company:
frappe.throw(
_("{0} {1}: Account {2} does not belong to Company {3}").format(
self.voucher_type, self.voucher_no, self.account, self.company
)
)
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
for key, value in dimension_filter_map.items():
dimension = key[0]
account = key[1]
if self.account == account:
if value["is_mandatory"] and not self.get(dimension):
frappe.throw(
_("{0} is mandatory for account {1}").format(
frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
),
MandatoryAccountDimensionError,
)
if value["allow_or_restrict"] == "Allow":
if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
else:
if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
frappe.throw(
_("Invalid value {0} for {1} against account {2}").format(
frappe.bold(self.get(dimension)),
frappe.bold(frappe.unscrub(dimension)),
frappe.bold(self.account),
),
InvalidAccountDimensionError,
)
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if (
account_type == "Profit and Loss"
and self.company == dimension.company
and dimension.mandatory_for_pl
and not dimension.disabled
):
if not self.get(dimension.fieldname):
frappe.throw(
_("Accounting Dimension <b>{0}</b> is required for 'Profit and Loss' account {1}.").format(
dimension.label, self.account
)
)
if (
account_type == "Balance Sheet"
and self.company == dimension.company
and dimension.mandatory_for_bs
and not dimension.disabled
):
if not self.get(dimension.fieldname):
frappe.throw(
_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.").format(
dimension.label, self.account
)
)
def validate(self):
self.validate_account()
def on_update(self):
adv_adj = self.flags.adv_adj
if not self.flags.from_repost:
self.validate_account_details()
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
# update outstanding amount
if (
self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
and self.flags.update_outstanding == "Yes"
and not frappe.flags.is_reverse_depr_entry
):
update_voucher_outstanding(
self.against_voucher_type, self.against_voucher_no, self.account, self.party_type, self.party
)
def on_doctype_update():
frappe.db.add_index("Payment Ledger Entry", ["against_voucher_no", "against_voucher_type"])
frappe.db.add_index("Payment Ledger Entry", ["voucher_no", "voucher_type"])

View File

@ -0,0 +1,408 @@
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.stock.doctype.item.test_item import create_item
class TestPaymentLedgerEntry(FrappeTestCase):
def setUp(self):
self.ple = qb.DocType("Payment Ledger Entry")
self.create_company()
self.create_item()
self.create_customer()
self.clear_old_entries()
def tearDown(self):
frappe.db.rollback()
def create_company(self):
company_name = "_Test Payment Ledger"
company = None
if frappe.db.exists("Company", company_name):
company = frappe.get_doc("Company", company_name)
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": company_name,
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PL"
self.income_account = "Sales - _PL"
self.expense_account = "Cost of Goods Sold - _PL"
self.debit_to = "Debtors - _PL"
self.creditors = "Creditors - _PL"
# create bank account
if frappe.db.exists("Account", "HDFC - _PL"):
self.bank = "HDFC - _PL"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PL",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item_name = "_Test PL Item"
item = create_item(
item_code=item_name, is_stock_item=0, company=self.company, warehouse=self.warehouse
)
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
name = "_Test PL Customer"
if frappe.db.exists("Customer", name):
self.customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.type = "Individual"
customer.save()
self.customer = customer.name
def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
sinv = create_sales_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return sinv
def create_payment_entry(self, amount=100, posting_date=nowdate()):
"""
Helper function to populate default values in payment entry
"""
payment = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=amount,
)
payment.posting_date = posting_date
return payment
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_journal_entry(
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
):
je = frappe.new_doc("Journal Entry")
je.posting_date = posting_date or nowdate()
je.company = self.company
je.user_remark = "test"
if not cost_center:
cost_center = self.cost_center
je.set(
"accounts",
[
{
"account": acc1,
"cost_center": cost_center,
"debit_in_account_currency": amount if amount > 0 else 0,
"credit_in_account_currency": abs(amount) if amount < 0 else 0,
},
{
"account": acc2,
"cost_center": cost_center,
"credit_in_account_currency": amount if amount > 0 else 0,
"debit_in_account_currency": abs(amount) if amount < 0 else 0,
},
],
)
return je
def test_payment_against_invoice(self):
transaction_date = nowdate()
amount = 100
ple = self.ple
# full payment using PE
si1 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
pe1 = get_payment_entry(si1.doctype, si1.name).save().submit()
pl_entries = (
qb.from_(ple)
.select(
ple.voucher_type,
ple.voucher_no,
ple.against_voucher_type,
ple.against_voucher_no,
ple.amount,
ple.delinked,
)
.where((ple.against_voucher_type == si1.doctype) & (ple.against_voucher_no == si1.name))
.orderby(ple.creation)
.run(as_dict=True)
)
expected_values = [
{
"voucher_type": si1.doctype,
"voucher_no": si1.name,
"against_voucher_type": si1.doctype,
"against_voucher_no": si1.name,
"amount": amount,
"delinked": 0,
},
{
"voucher_type": pe1.doctype,
"voucher_no": pe1.name,
"against_voucher_type": si1.doctype,
"against_voucher_no": si1.name,
"amount": -amount,
"delinked": 0,
},
]
self.assertEqual(pl_entries[0], expected_values[0])
self.assertEqual(pl_entries[1], expected_values[1])
def test_partial_payment_against_invoice(self):
ple = self.ple
transaction_date = nowdate()
amount = 100
# partial payment of invoice using PE
si2 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
pe2 = get_payment_entry(si2.doctype, si2.name)
pe2.get("references")[0].allocated_amount = 50
pe2.get("references")[0].outstanding_amount = 50
pe2 = pe2.save().submit()
pl_entries = (
qb.from_(ple)
.select(
ple.voucher_type,
ple.voucher_no,
ple.against_voucher_type,
ple.against_voucher_no,
ple.amount,
ple.delinked,
)
.where((ple.against_voucher_type == si2.doctype) & (ple.against_voucher_no == si2.name))
.orderby(ple.creation)
.run(as_dict=True)
)
expected_values = [
{
"voucher_type": si2.doctype,
"voucher_no": si2.name,
"against_voucher_type": si2.doctype,
"against_voucher_no": si2.name,
"amount": amount,
"delinked": 0,
},
{
"voucher_type": pe2.doctype,
"voucher_no": pe2.name,
"against_voucher_type": si2.doctype,
"against_voucher_no": si2.name,
"amount": -50,
"delinked": 0,
},
]
self.assertEqual(pl_entries[0], expected_values[0])
self.assertEqual(pl_entries[1], expected_values[1])
def test_cr_note_against_invoice(self):
ple = self.ple
transaction_date = nowdate()
amount = 100
# reconcile against return invoice
si3 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note1 = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note1.is_return = 1
cr_note1.return_against = si3.name
cr_note1 = cr_note1.save().submit()
pl_entries = (
qb.from_(ple)
.select(
ple.voucher_type,
ple.voucher_no,
ple.against_voucher_type,
ple.against_voucher_no,
ple.amount,
ple.delinked,
)
.where((ple.against_voucher_type == si3.doctype) & (ple.against_voucher_no == si3.name))
.orderby(ple.creation)
.run(as_dict=True)
)
expected_values = [
{
"voucher_type": si3.doctype,
"voucher_no": si3.name,
"against_voucher_type": si3.doctype,
"against_voucher_no": si3.name,
"amount": amount,
"delinked": 0,
},
{
"voucher_type": cr_note1.doctype,
"voucher_no": cr_note1.name,
"against_voucher_type": si3.doctype,
"against_voucher_no": si3.name,
"amount": -amount,
"delinked": 0,
},
]
self.assertEqual(pl_entries[0], expected_values[0])
self.assertEqual(pl_entries[1], expected_values[1])
def test_je_against_inv_and_note(self):
ple = self.ple
transaction_date = nowdate()
amount = 100
# reconcile against return invoice using JE
si4 = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note2 = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note2.is_return = 1
cr_note2 = cr_note2.save().submit()
je1 = self.create_journal_entry(
self.debit_to, self.debit_to, amount, posting_date=transaction_date
)
je1.get("accounts")[0].party_type = je1.get("accounts")[1].party_type = "Customer"
je1.get("accounts")[0].party = je1.get("accounts")[1].party = self.customer
je1.get("accounts")[0].reference_type = cr_note2.doctype
je1.get("accounts")[0].reference_name = cr_note2.name
je1.get("accounts")[1].reference_type = si4.doctype
je1.get("accounts")[1].reference_name = si4.name
je1 = je1.save().submit()
pl_entries_for_invoice = (
qb.from_(ple)
.select(
ple.voucher_type,
ple.voucher_no,
ple.against_voucher_type,
ple.against_voucher_no,
ple.amount,
ple.delinked,
)
.where((ple.against_voucher_type == si4.doctype) & (ple.against_voucher_no == si4.name))
.orderby(ple.creation)
.run(as_dict=True)
)
expected_values = [
{
"voucher_type": si4.doctype,
"voucher_no": si4.name,
"against_voucher_type": si4.doctype,
"against_voucher_no": si4.name,
"amount": amount,
"delinked": 0,
},
{
"voucher_type": je1.doctype,
"voucher_no": je1.name,
"against_voucher_type": si4.doctype,
"against_voucher_no": si4.name,
"amount": -amount,
"delinked": 0,
},
]
self.assertEqual(pl_entries_for_invoice[0], expected_values[0])
self.assertEqual(pl_entries_for_invoice[1], expected_values[1])
pl_entries_for_crnote = (
qb.from_(ple)
.select(
ple.voucher_type,
ple.voucher_no,
ple.against_voucher_type,
ple.against_voucher_no,
ple.amount,
ple.delinked,
)
.where(
(ple.against_voucher_type == cr_note2.doctype) & (ple.against_voucher_no == cr_note2.name)
)
.orderby(ple.creation)
.run(as_dict=True)
)
expected_values = [
{
"voucher_type": cr_note2.doctype,
"voucher_no": cr_note2.name,
"against_voucher_type": cr_note2.doctype,
"against_voucher_no": cr_note2.name,
"amount": -amount,
"delinked": 0,
},
{
"voucher_type": je1.doctype,
"voucher_no": je1.name,
"against_voucher_type": cr_note2.doctype,
"against_voucher_no": cr_note2.name,
"amount": amount,
"delinked": 0,
},
]
self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
self.assertEqual(pl_entries_for_crnote[1], expected_values[1])

View File

@ -39,7 +39,7 @@ def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(
""" select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
limit %(start)s, %(page_len)s""",
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
)
@ -51,7 +51,7 @@ def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
""" select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
limit %(start)s, %(page_len)s""",
limit %(page_len)s offset %(start)s""",
{"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
)

View File

@ -3,16 +3,26 @@
import frappe
from frappe import _, msgprint
from frappe import _, msgprint, qb
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, getdate, nowdate, today
import erpnext
from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
from erpnext.accounts.utils import (
QueryPaymentLedger,
get_outstanding_invoices,
reconcile_against_document,
)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document):
def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = []
@frappe.whitelist()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()
@ -108,54 +118,58 @@ class PaymentReconciliation(Document):
return list(journal_entries)
def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True)
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "debit_in_account_currency"
)
reconciled_dr_or_cr = (
"debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
self.build_qb_filter_conditions(get_return_invoices=True)
ple = qb.DocType("Payment Ledger Entry")
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
return frappe.db.sql(
""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl
WHERE
(doc.name = gl.against_voucher or doc.name = gl.voucher_no)
and doc.{party_type_field} = %(party)s
and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
and gl.against_voucher_type = %(voucher_type)s
and doc.docstatus = 1 and gl.party = %(party)s
and gl.party_type = %(party_type)s and gl.account = %(account)s
and gl.is_cancelled = 0 {condition}
GROUP BY doc.name
Having
amount > 0
ORDER BY doc.posting_date
""".format(
doc=voucher_type,
dr_or_cr=dr_or_cr,
reconciled_dr_or_cr=reconciled_dr_or_cr,
party_type_field=frappe.scrub(self.party_type),
condition=condition or "",
),
{
"party": self.party,
"party_type": self.party_type,
"voucher_type": voucher_type,
"account": self.receivable_payable_account,
},
as_dict=1,
if erpnext.get_party_account_type(self.party_type) == "Receivable":
self.common_filter_conditions.append(ple.account_type == "Receivable")
else:
self.common_filter_conditions.append(ple.account_type == "Payable")
self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
# get return invoices
doc = qb.DocType(voucher_type)
return_invoices = (
qb.from_(doc)
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
& (IfNull(doc.return_against, "") == "")
)
.run(as_dict=True)
)
outstanding_dr_or_cr = []
if return_invoices:
ple_query = QueryPaymentLedger()
return_outstanding = ple_query.get_voucher_outstandings(
vouchers=return_invoices,
common_filter=self.common_filter_conditions,
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
get_payments=True,
)
for inv in return_outstanding:
if inv.outstanding != 0:
outstanding_dr_or_cr.append(
frappe._dict(
{
"reference_type": inv.voucher_type,
"reference_name": inv.voucher_no,
"amount": -(inv.outstanding_in_account_currency),
"posting_date": inv.posting_date,
"currency": inv.currency,
}
)
)
return outstanding_dr_or_cr
def add_payment_entries(self, non_reconciled_payments):
self.set("payments", [])
@ -166,10 +180,15 @@ class PaymentReconciliation(Document):
def get_invoice_entries(self):
# Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
condition = self.get_conditions(get_invoices=True)
self.build_qb_filter_conditions(get_invoices=True)
non_reconciled_invoices = get_outstanding_invoices(
self.party_type, self.party, self.receivable_payable_account, condition=condition
self.party_type,
self.party,
self.receivable_payable_account,
common_filter=self.common_filter_conditions,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
)
if self.invoice_limit:
@ -329,89 +348,56 @@ class PaymentReconciliation(Document):
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company)
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear()
ple = qb.DocType("Payment Ledger Entry")
if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
condition = " and cost_center = '{0}' ".format(self.cost_center)
self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices):
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
if self.from_invoice_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
if self.to_invoice_date
else ""
)
dr_or_cr = (
"debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "credit_in_account_currency"
)
if self.minimum_invoice_amount:
condition += " and {dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
)
if self.maximum_invoice_amount:
condition += " and {dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
)
if self.from_invoice_date:
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
if self.to_invoice_date:
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
condition += (
" and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
dr_or_cr = (
"debit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
else "credit_in_account_currency"
)
if self.from_payment_date:
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
if self.to_payment_date:
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
if self.minimum_invoice_amount:
condition += " and gl.{dr_or_cr} >= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
)
if self.maximum_invoice_amount:
condition += " and gl.{dr_or_cr} <= {amount}".format(
dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
)
def get_conditions(self, get_payments=False):
condition = " and company = '{0}' ".format(self.company)
else:
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.get("cost_center") and get_payments:
condition = " and cost_center = '{0}' ".format(self.cost_center)
if self.minimum_payment_amount:
condition += (
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount:
condition += (
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
condition += (
" and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
if self.from_payment_date
else ""
)
condition += (
" and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
if self.to_payment_date
else ""
)
if self.minimum_payment_amount:
condition += (
" and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
)
if self.maximum_payment_amount:
condition += (
" and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
)
return condition

View File

@ -4,93 +4,542 @@
import unittest
import frappe
from frappe.utils import add_days, getdate
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
from erpnext.stock.doctype.item.test_item import create_item
class TestPaymentReconciliation(unittest.TestCase):
@classmethod
def setUpClass(cls):
make_customer()
make_invoice_and_payment()
class TestPaymentReconciliation(FrappeTestCase):
def setUp(self):
self.create_company()
self.create_item()
self.create_customer()
self.create_account()
self.clear_old_entries()
def test_payment_reconciliation(self):
payment_reco = frappe.get_doc("Payment Reconciliation")
payment_reco.company = "_Test Company"
payment_reco.party_type = "Customer"
payment_reco.party = "_Test Payment Reco Customer"
payment_reco.receivable_payable_account = "Debtors - _TC"
payment_reco.from_invoice_date = add_days(getdate(), -1)
payment_reco.to_invoice_date = getdate()
payment_reco.from_payment_date = add_days(getdate(), -1)
payment_reco.to_payment_date = getdate()
payment_reco.maximum_invoice_amount = 1000
payment_reco.maximum_payment_amount = 1000
payment_reco.invoice_limit = 10
payment_reco.payment_limit = 10
payment_reco.bank_cash_account = "_Test Bank - _TC"
payment_reco.cost_center = "_Test Cost Center - _TC"
payment_reco.get_unreconciled_entries()
def tearDown(self):
frappe.db.rollback()
self.assertEqual(len(payment_reco.get("invoices")), 1)
self.assertEqual(len(payment_reco.get("payments")), 1)
def create_company(self):
company = None
if frappe.db.exists("Company", "_Test Payment Reconciliation"):
company = frappe.get_doc("Company", "_Test Payment Reconciliation")
else:
company = frappe.get_doc(
{
"doctype": "Company",
"company_name": "_Test Payment Reconciliation",
"country": "India",
"default_currency": "INR",
"create_chart_of_accounts_based_on": "Standard Template",
"chart_of_accounts": "Standard",
}
)
company = company.save()
payment_entry = payment_reco.get("payments")[0].reference_name
invoice = payment_reco.get("invoices")[0].invoice_number
self.company = company.name
self.cost_center = company.cost_center
self.warehouse = "All Warehouses - _PR"
self.income_account = "Sales - _PR"
self.expense_account = "Cost of Goods Sold - _PR"
self.debit_to = "Debtors - _PR"
self.creditors = "Creditors - _PR"
payment_reco.allocate_entries(
{
"payments": [payment_reco.get("payments")[0].as_dict()],
"invoices": [payment_reco.get("invoices")[0].as_dict()],
}
# create bank account
if frappe.db.exists("Account", "HDFC - _PR"):
self.bank = "HDFC - _PR"
else:
bank_acc = frappe.get_doc(
{
"doctype": "Account",
"account_name": "HDFC",
"parent_account": "Bank Accounts - _PR",
"company": self.company,
}
)
bank_acc.save()
self.bank = bank_acc.name
def create_item(self):
item = create_item(
item_code="_Test PR Item", is_stock_item=0, company=self.company, warehouse=self.warehouse
)
payment_reco.reconcile()
self.item = item if isinstance(item, str) else item.item_code
payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
def create_customer(self):
if frappe.db.exists("Customer", "_Test PR Customer"):
self.customer = "_Test PR Customer"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer"
customer.type = "Individual"
customer.save()
self.customer = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 2"):
self.customer2 = "_Test PR Customer 2"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 2"
customer.type = "Individual"
customer.save()
self.customer2 = customer.name
def make_customer():
if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
frappe.get_doc(
{
"doctype": "Customer",
"customer_name": "_Test Payment Reco Customer",
"customer_type": "Individual",
"customer_group": "_Test Customer Group",
"territory": "_Test Territory",
}
).insert()
if frappe.db.exists("Customer", "_Test PR Customer 3"):
self.customer3 = "_Test PR Customer 3"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 3"
customer.type = "Individual"
customer.default_currency = "EUR"
customer.save()
self.customer3 = customer.name
def create_account(self):
account_name = "Debtors EUR"
if not frappe.db.get_value(
"Account", filters={"account_name": account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = account_name
acc.parent_account = "Accounts Receivable - _PR"
acc.company = self.company
acc.account_currency = "EUR"
acc.account_type = "Receivable"
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
self.debtors_eur = acc.name
def make_invoice_and_payment():
si = create_sales_invoice(
customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
)
si.cost_center = "_Test Cost Center - _TC"
si.save()
si.submit()
def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
sinv = create_sales_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.customer,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return sinv
pe = frappe.get_doc(
{
"doctype": "Payment Entry",
"payment_type": "Receive",
"party_type": "Customer",
"party": "_Test Payment Reco Customer",
"company": "_Test Company",
"paid_from_account_currency": "INR",
"paid_to_account_currency": "INR",
"source_exchange_rate": 1,
"target_exchange_rate": 1,
"reference_no": "1",
"reference_date": getdate(),
"received_amount": 690,
"paid_amount": 690,
"paid_from": "Debtors - _TC",
"paid_to": "_Test Bank - _TC",
"cost_center": "_Test Cost Center - _TC",
}
)
pe.insert()
pe.submit()
def create_payment_entry(self, amount=100, posting_date=nowdate(), customer=None):
"""
Helper function to populate default values in payment entry
"""
payment = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=customer or self.customer,
paid_from=self.debit_to,
paid_to=self.bank,
paid_amount=amount,
)
payment.posting_date = posting_date
return payment
def clear_old_entries(self):
doctype_list = [
"GL Entry",
"Payment Ledger Entry",
"Sales Invoice",
"Purchase Invoice",
"Payment Entry",
"Journal Entry",
]
for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self):
pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Customer"
pr.party = self.customer
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
return pr
def create_journal_entry(
self, acc1=None, acc2=None, amount=0, posting_date=None, cost_center=None
):
je = frappe.new_doc("Journal Entry")
je.posting_date = posting_date or nowdate()
je.company = self.company
je.user_remark = "test"
if not cost_center:
cost_center = self.cost_center
je.set(
"accounts",
[
{
"account": acc1,
"cost_center": cost_center,
"debit_in_account_currency": amount if amount > 0 else 0,
"credit_in_account_currency": abs(amount) if amount < 0 else 0,
},
{
"account": acc2,
"cost_center": cost_center,
"credit_in_account_currency": amount if amount > 0 else 0,
"debit_in_account_currency": abs(amount) if amount < 0 else 0,
},
],
)
return je
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
self.create_sales_invoice(qty=1, rate=400)
self.create_sales_invoice(qty=1, rate=500)
self.create_payment_entry(amount=300).save().submit()
self.create_payment_entry(amount=400).save().submit()
self.create_payment_entry(amount=500).save().submit()
pr = self.create_payment_reconciliation()
pr.minimum_invoice_amount = 400
pr.maximum_invoice_amount = 500
pr.minimum_payment_amount = 300
pr.maximum_payment_amount = 600
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 3)
pr.minimum_invoice_amount = 300
pr.maximum_invoice_amount = 600
pr.minimum_payment_amount = 400
pr.maximum_payment_amount = 500
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 3)
self.assertEqual(len(pr.get("payments")), 2)
pr.minimum_invoice_amount = (
pr.maximum_invoice_amount
) = pr.minimum_payment_amount = pr.maximum_payment_amount = 0
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 3)
self.assertEqual(len(pr.get("payments")), 3)
def test_filter_posting_date(self):
# check filter condition using transaction date
date1 = nowdate()
date2 = add_days(nowdate(), -1)
amount = 100
self.create_sales_invoice(qty=1, rate=amount, posting_date=date1)
si2 = self.create_sales_invoice(
qty=1, rate=amount, posting_date=date2, do_not_save=True, do_not_submit=True
)
si2.set_posting_time = 1
si2.posting_date = date2
si2.save().submit()
self.create_payment_entry(amount=amount, posting_date=date1).save().submit()
self.create_payment_entry(amount=amount, posting_date=date2).save().submit()
pr = self.create_payment_reconciliation()
pr.from_invoice_date = pr.to_invoice_date = date1
pr.from_payment_date = pr.to_payment_date = date1
pr.get_unreconciled_entries()
# assert only si and pe are fetched
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
pr.from_invoice_date = date2
pr.to_invoice_date = date1
pr.from_payment_date = date2
pr.to_payment_date = date1
pr.get_unreconciled_entries()
# assert only si and pe are fetched
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 2)
def test_filter_invoice_limit(self):
# check filter condition - invoice limit
transaction_date = nowdate()
rate = 100
invoices = []
payments = []
for i in range(5):
invoices.append(self.create_sales_invoice(qty=1, rate=rate, posting_date=transaction_date))
pe = self.create_payment_entry(amount=rate, posting_date=transaction_date).save().submit()
payments.append(pe)
pr = self.create_payment_reconciliation()
pr.from_invoice_date = pr.to_invoice_date = transaction_date
pr.from_payment_date = pr.to_payment_date = transaction_date
pr.invoice_limit = 2
pr.payment_limit = 3
pr.get_unreconciled_entries()
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 3)
def test_payment_against_invoice(self):
si = self.create_sales_invoice(qty=1, rate=200)
pe = self.create_payment_entry(amount=55).save().submit()
# second payment entry
self.create_payment_entry(amount=35).save().submit()
pr = self.create_payment_reconciliation()
# reconcile multiple payments against invoice
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
si.reload()
self.assertEqual(si.status, "Partly Paid")
# check PR tool output post reconciliation
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 110)
self.assertEqual(pr.get("payments"), [])
# cancel one PE
pe.reload()
pe.cancel()
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 0)
self.assertEqual(pr.get("invoices")[0].get("outstanding_amount"), 165)
def test_payment_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
amount = 921
# debit debtors account to record an invoice
je = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
je.accounts[0].party_type = "Customer"
je.accounts[0].party = self.customer
je.save()
je.submit()
self.create_payment_entry(amount=amount, posting_date=transaction_date).save().submit()
pr = self.create_payment_reconciliation()
pr.minimum_invoice_amount = pr.maximum_invoice_amount = amount
pr.from_invoice_date = pr.to_invoice_date = transaction_date
pr.from_payment_date = pr.to_payment_date = transaction_date
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
# credit debtors account to record a payment
je = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je.accounts[1].party_type = "Customer"
je.accounts[1].party = self.customer
je.save()
je.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_journal_against_journal(self):
transaction_date = nowdate()
sales = "Sales - _PR"
amount = 100
# debit debtors account to simulate a invoice
je1 = self.create_journal_entry(self.debit_to, sales, amount, transaction_date)
je1.accounts[0].party_type = "Customer"
je1.accounts[0].party = self.customer
je1.save()
je1.submit()
# credit debtors account to simulate a payment
je2 = self.create_journal_entry(self.bank, self.debit_to, amount, transaction_date)
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
def test_cr_note_against_invoice(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
pr.get_unreconciled_entries()
# check reconciliation tool output
# reconciled invoice and credit note shouldn't show up in selection
self.assertEqual(pr.get("invoices"), [])
self.assertEqual(pr.get("payments"), [])
# assert outstanding
si.reload()
self.assertEqual(si.status, "Paid")
self.assertEqual(si.outstanding_amount, 0)
def test_cr_note_partial_against_invoice(self):
transaction_date = nowdate()
amount = 100
allocated_amount = 80
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.is_return = 1
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = allocated_amount
pr.reconcile()
# assert outstanding
si.reload()
self.assertEqual(si.status, "Partly Paid")
self.assertEqual(si.outstanding_amount, 20)
pr.get_unreconciled_entries()
# check reconciliation tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(len(pr.get("payments")), 1)
self.assertEqual(pr.get("invoices")[0].outstanding_amount, 20)
self.assertEqual(pr.get("payments")[0].amount, 20)
def test_pr_output_foreign_currency_and_amount(self):
# test for currency and amount invoices and payments
transaction_date = nowdate()
# In EUR
amount = 100
exchange_rate = 80
si = self.create_sales_invoice(
qty=1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
si.customer = self.customer3
si.currency = "EUR"
si.conversion_rate = exchange_rate
si.debit_to = self.debtors_eur
si = si.save().submit()
cr_note = self.create_sales_invoice(
qty=-1, rate=amount, posting_date=transaction_date, do_not_save=True, do_not_submit=True
)
cr_note.customer = self.customer3
cr_note.is_return = 1
cr_note.currency = "EUR"
cr_note.conversion_rate = exchange_rate
cr_note.debit_to = self.debtors_eur
cr_note = cr_note.save().submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer3
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.invoices[0].amount, amount)
self.assertEqual(pr.invoices[0].currency, "EUR")
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
cr_note.cancel()
pay = self.create_payment_entry(
amount=amount, posting_date=transaction_date, customer=self.customer3
)
pay.paid_from = self.debtors_eur
pay.paid_from_account_currency = "EUR"
pay.source_exchange_rate = exchange_rate
pay.received_amount = exchange_rate * amount
pay = pay.save().submit()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")

View File

@ -10,10 +10,11 @@
"fiscal_year",
"amended_from",
"company",
"cost_center_wise_pnl",
"column_break1",
"closing_account_head",
"remarks"
"remarks",
"gle_processing_status",
"error_message"
],
"fields": [
{
@ -86,17 +87,26 @@
"reqd": 1
},
{
"default": "0",
"fieldname": "cost_center_wise_pnl",
"fieldtype": "Check",
"label": "Book Cost Center Wise Profit/Loss"
"depends_on": "eval:doc.docstatus!=0",
"fieldname": "gle_processing_status",
"fieldtype": "Select",
"label": "GL Entry Processing Status",
"options": "In Progress\nCompleted\nFailed",
"read_only": 1
},
{
"depends_on": "eval:doc.gle_processing_status=='Failed'",
"fieldname": "error_message",
"fieldtype": "Text",
"label": "Error Message",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-20 15:27:37.210458",
"modified": "2022-07-20 14:51:04.714154",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Period Closing Voucher",

View File

@ -8,7 +8,6 @@ from frappe.utils import flt
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
get_dimensions,
)
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
@ -20,13 +19,28 @@ class PeriodClosingVoucher(AccountsController):
self.validate_posting_date()
def on_submit(self):
self.db_set("gle_processing_status", "In Progress")
self.make_gl_entries()
def on_cancel(self):
self.db_set("gle_processing_status", "In Progress")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
from erpnext.accounts.general_ledger import make_reverse_gl_entries
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
gle_count = frappe.db.count(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0},
)
if gle_count > 5000:
frappe.enqueue(
make_reverse_gl_entries,
voucher_type="Period Closing Voucher",
voucher_no=self.name,
queue="long",
)
frappe.msgprint(
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True
)
else:
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_account_head(self):
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
@ -54,8 +68,8 @@ class PeriodClosingVoucher(AccountsController):
pce = frappe.db.sql(
"""select name from `tabPeriod Closing Voucher`
where posting_date > %s and fiscal_year = %s and docstatus = 1""",
(self.posting_date, self.fiscal_year),
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
(self.posting_date, self.fiscal_year, self.company),
)
if pce and pce[0][0]:
frappe.throw(
@ -67,90 +81,80 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self):
gl_entries = self.get_gl_entries()
if gl_entries:
from erpnext.accounts.general_ledger import make_gl_entries
make_gl_entries(gl_entries)
if len(gl_entries) > 5000:
frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long")
frappe.msgprint(
_("The GL Entries will be processed in the background, it can take a few minutes."),
alert=True,
)
else:
process_gl_entries(gl_entries)
def get_gl_entries(self):
gl_entries = []
pl_accounts = self.get_pl_balances()
for acc in pl_accounts:
# pl account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True):
if flt(acc.bal_in_company_currency):
gl_entries.append(
self.get_gl_dict(
{
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
},
item=acc,
)
)
gl_entries.append(self.get_gle_for_pl_account(acc))
if gl_entries:
gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts)
gl_entries += gle_for_net_pl_bal
# closing liability account
for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False):
if flt(acc.bal_in_company_currency):
gl_entries.append(self.get_gle_for_closing_account(acc))
return gl_entries
def get_pnl_gl_entry(self, pl_accounts):
company_cost_center = frappe.db.get_value("Company", self.company, "cost_center")
gl_entries = []
def get_gle_for_pl_account(self, acc):
gl_entry = self.get_gl_dict(
{
"account": acc.account,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
for acc in pl_accounts:
if flt(acc.bal_in_company_currency):
cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center
gl_entry = self.get_gl_dict(
{
"account": self.closing_account_head,
"cost_center": cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) > 0
else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency))
if flt(acc.bal_in_company_currency) < 0
else 0,
},
item=acc,
)
def get_gle_for_closing_account(self, acc):
gl_entry = self.get_gl_dict(
{
"account": self.closing_account_head,
"cost_center": acc.cost_center,
"finance_book": acc.finance_book,
"account_currency": acc.account_currency,
"debit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) > 0
else 0,
"debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0,
"credit_in_account_currency": abs(flt(acc.bal_in_account_currency))
if flt(acc.bal_in_account_currency) < 0
else 0,
"credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0,
},
item=acc,
)
self.update_default_dimensions(gl_entry, acc)
return gl_entry
self.update_default_dimensions(gl_entry)
gl_entries.append(gl_entry)
return gl_entries
def update_default_dimensions(self, gl_entry):
def update_default_dimensions(self, gl_entry, acc):
if not self.accounting_dimensions:
self.accounting_dimensions = get_accounting_dimensions()
_, default_dimensions = get_dimensions()
for dimension in self.accounting_dimensions:
gl_entry.update({dimension: default_dimensions.get(self.company, {}).get(dimension)})
gl_entry.update({dimension: acc.get(dimension)})
def get_pl_balances(self):
def get_pl_balances_based_on_dimensions(self, group_by_account=False):
"""Get balance for dimension-wise pl accounts"""
dimension_fields = ["t1.cost_center", "t1.finance_book"]
@ -159,20 +163,56 @@ class PeriodClosingVoucher(AccountsController):
for dimension in self.accounting_dimensions:
dimension_fields.append("t1.{0}".format(dimension))
if group_by_account:
dimension_fields.append("t1.account")
return frappe.db.sql(
"""
select
t1.account, t2.account_currency, {dimension_fields},
t2.account_currency,
{dimension_fields},
sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency,
sum(t1.debit) - sum(t1.credit) as bal_in_company_currency
from `tabGL Entry` t1, `tabAccount` t2
where t1.is_cancelled = 0 and t1.account = t2.name and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2 and t2.company = %s
and t1.posting_date between %s and %s
group by t1.account, {dimension_fields}
where
t1.is_cancelled = 0
and t1.account = t2.name
and t2.report_type = 'Profit and Loss'
and t2.docstatus < 2
and t2.company = %s
and t1.posting_date between %s and %s
group by {dimension_fields}
""".format(
dimension_fields=", ".join(dimension_fields)
),
(self.company, self.get("year_start_date"), self.posting_date),
as_dict=1,
)
def process_gl_entries(gl_entries):
from erpnext.accounts.general_ledger import make_gl_entries
try:
make_gl_entries(gl_entries, merge_entries=False)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed"
)
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
)
def make_reverse_gl_entries(voucher_type, voucher_no):
from erpnext.accounts.general_ledger import make_reverse_gl_entries
try:
make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed")
except Exception as e:
frappe.db.rollback()
frappe.log_error(e)
frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed")

View File

@ -49,7 +49,7 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expected_gle = (
("Cost of Goods Sold - TPC", 0.0, 600.0),
(surplus_account, 600.0, 400.0),
(surplus_account, 200.0, 0.0),
("Sales - TPC", 400.0, 0.0),
)
@ -59,7 +59,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
""",
(pcv.name),
)
pcv.reload()
self.assertEqual(pcv.gle_processing_status, "Completed")
self.assertEqual(pcv_gle, expected_gle)
def test_cost_center_wise_posting(self):
@ -78,6 +79,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC",
rate=400,
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
)
create_sales_invoice(
company=company,
@ -86,10 +89,11 @@ class TestPeriodClosingVoucher(unittest.TestCase):
expense_account="Cost of Goods Sold - TPC",
rate=200,
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
)
pcv = self.make_period_closing_voucher(submit=False)
pcv.cost_center_wise_pnl = 1
pcv.save()
pcv.submit()
surplus_account = pcv.closing_account_head
@ -112,6 +116,16 @@ class TestPeriodClosingVoucher(unittest.TestCase):
self.assertEqual(pcv_gle, expected_gle)
pcv.reload()
pcv.cancel()
self.assertFalse(
frappe.db.get_value(
"GL Entry",
{"voucher_type": "Period Closing Voucher", "voucher_no": pcv.name, "is_cancelled": 0},
)
)
def test_period_closing_with_finance_book_entries(self):
frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'")
@ -119,14 +133,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
surplus_account = create_account()
cost_center = create_cost_center("Test Cost Center 1")
create_sales_invoice(
si = create_sales_invoice(
company=company,
income_account="Sales - TPC",
expense_account="Cost of Goods Sold - TPC",
cost_center=cost_center,
rate=400,
debit_to="Debtors - TPC",
currency="USD",
customer="_Test Customer USD",
)
jv = make_journal_entry(
account1="Cash - TPC",
account2="Sales - TPC",

View File

@ -64,13 +64,15 @@ frappe.ui.form.on('POS Closing Entry', {
pos_opening_entry(frm) {
if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) {
reset_values(frm);
frm.trigger("set_opening_amounts");
frm.trigger("get_pos_invoices");
frappe.run_serially([
() => frm.trigger("set_opening_amounts"),
() => frm.trigger("get_pos_invoices")
]);
}
},
set_opening_amounts(frm) {
frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry)
.then(({ balance_details }) => {
balance_details.forEach(detail => {
frm.add_child("payment_reconciliation", {
@ -83,7 +85,7 @@ frappe.ui.form.on('POS Closing Entry', {
},
get_pos_invoices(frm) {
frappe.call({
return frappe.call({
method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices',
args: {
start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date),
@ -100,7 +102,9 @@ frappe.ui.form.on('POS Closing Entry', {
});
},
before_save: function(frm) {
before_save: async function(frm) {
frappe.dom.freeze(__('Processing Sales! Please Wait...'));
frm.set_value("grand_total", 0);
frm.set_value("net_total", 0);
frm.set_value("total_quantity", 0);
@ -110,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', {
row.expected_amount = row.opening_amount;
}
for (let row of frm.doc.pos_transactions) {
frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
});
const pos_inv_promises = frm.doc.pos_transactions.map(
row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
);
const pos_invoices = await Promise.all(pos_inv_promises);
for (let doc of pos_invoices) {
frm.doc.grand_total += flt(doc.grand_total);
frm.doc.net_total += flt(doc.net_total);
frm.doc.total_quantity += flt(doc.total_qty);
refresh_payments(doc, frm);
refresh_taxes(doc, frm);
refresh_fields(frm);
set_html_data(frm);
}
frappe.dom.unfreeze();
}
});

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

@ -96,6 +96,7 @@ class POSInvoice(SalesInvoice):
)
def on_cancel(self):
self.ignore_linked_doctypes = "Payment Ledger Entry"
# run on cancel method of selling controller
super(SalesInvoice, self).on_cancel()
if not self.is_return and self.loyalty_program:
@ -221,9 +222,6 @@ class POSInvoice(SalesInvoice):
from erpnext.stock.stock_ledger import is_negative_stock_allowed
for d in self.get("items"):
is_service_item = not (frappe.db.get_value("Item", d.get("item_code"), "is_stock_item"))
if is_service_item:
return
if d.serial_no:
self.validate_pos_reserved_serial_nos(d)
self.validate_delivered_serial_nos(d)

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.utils import flt, getdate, nowdate
from frappe.utils import cint, flt, getdate, nowdate
from frappe.utils.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive
@ -219,6 +219,9 @@ class POSInvoiceMergeLog(Document):
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
invoice.customer = self.customer
invoice.disable_rounded_total = cint(
frappe.db.get_value("POS Profile", invoice.pos_profile, "disable_rounded_total")
)
if self.merge_invoices_based_on == "Customer Group":
invoice.flags.ignore_pos_profile = True

View File

@ -44,6 +44,7 @@
"write_off_account",
"write_off_cost_center",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
"income_account",
"expense_account",
@ -358,6 +359,13 @@
"fieldname": "validate_stock_on_save",
"fieldtype": "Check",
"label": "Validate Stock on Save"
},
{
"default": "0",
"description": "If enabled, the consolidated invoices will have rounded total disabled",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
}
],
"icon": "icon-cog",
@ -385,7 +393,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2022-03-21 13:29:28.480533",
"modified": "2022-07-21 11:16:46.911173",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@ -173,7 +173,7 @@ def pos_profile_query(doctype, txt, searchfield, start, page_len, filters):
where
pfu.parent = pf.name and pfu.user = %(user)s and pf.company = %(company)s
and (pf.name like %(txt)s)
and pf.disabled = 0 limit %(start)s, %(page_len)s""",
and pf.disabled = 0 limit %(page_len)s offset %(start)s""",
args,
)

View File

@ -36,8 +36,12 @@ class PricingRule(Document):
def validate_duplicate_apply_on(self):
if self.apply_on != "Transaction":
field = apply_on_dict.get(self.apply_on)
values = [d.get(frappe.scrub(self.apply_on)) for d in self.get(field) if field]
apply_on_table = apply_on_dict.get(self.apply_on)
if not apply_on_table:
return
apply_on_field = frappe.scrub(self.apply_on)
values = [d.get(apply_on_field) for d in self.get(apply_on_table) if d.get(apply_on_field)]
if len(values) != len(set(values)):
frappe.throw(_("Duplicate {0} found in the table").format(self.apply_on))

View File

@ -752,7 +752,7 @@ class TestPricingRule(unittest.TestCase):
title="_Test Pricing Rule with Min Qty - 2",
)
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1, currency="USD")
si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", qty=1)
item = si.items[0]
item.stock_qty = 1
si.save()

View File

@ -45,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
refresh(doc) {
@ -541,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'));
@ -574,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

@ -165,17 +165,6 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).set_missing_values(for_validate)
def check_conversion_rate(self):
default_currency = erpnext.get_company_currency(self.company)
if not default_currency:
throw(_("Please enter default currency in Company Master"))
if (
(self.currency == default_currency and flt(self.conversion_rate) != 1.00)
or not self.conversion_rate
or (self.currency != default_currency and flt(self.conversion_rate) == 1.00)
):
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self):
if not self.credit_to:
self.credit_to = get_party_account("Supplier", self.supplier, self.company)
@ -513,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")
@ -545,7 +537,16 @@ class PurchaseInvoice(BuyingController):
from_repost=from_repost,
)
elif self.docstatus == 2:
provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"]
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
if provisional_entries:
for entry in provisional_entries:
frappe.db.set_value(
"GL Entry",
{"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no},
"is_cancelled",
1,
)
if update_outstanding == "No":
update_outstanding_amt(
@ -1127,7 +1128,7 @@ class PurchaseInvoice(BuyingController):
# Stock ledger value is not matching with the warehouse amount
if (
self.update_stock
and voucher_wise_stock_value.get(item.name)
and voucher_wise_stock_value.get((item.name, item.warehouse))
and warehouse_debit_amount
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
):
@ -1407,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()
@ -1418,7 +1421,12 @@ class PurchaseInvoice(BuyingController):
frappe.db.set(self, "status", "Cancelled")
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Payment Ledger Entry",
)
self.update_advance_tax_references(cancel=1)
def update_project(self):

View File

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

View File

@ -27,12 +27,13 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
make_purchase_receipt,
)
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
from erpnext.stock.tests.test_utils import StockTestMixin
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
test_ignore = ["Serial No"]
class TestPurchaseInvoice(unittest.TestCase):
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
@classmethod
def setUpClass(self):
unlink_payment_on_cancel_of_invoice()
@ -469,37 +470,6 @@ class TestPurchaseInvoice(unittest.TestCase):
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,
@ -693,6 +663,80 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
def test_standalone_return_using_pi(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
item = self.make_item().name
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120)
return_pi = make_purchase_invoice(
is_return=1,
item=item,
qty=-10,
update_stock=1,
rate=100,
company=company,
warehouse=warehouse,
cost_center="Main - TCP1",
)
# assert that stock consumption is with actual rate
self.assertGLEs(
return_pi,
[{"credit": 1200, "debit": 0}],
gle_filters={"account": "Stock In Hand - TCP1"},
)
# assert loss booked in COGS
self.assertGLEs(
return_pi,
[{"credit": 0, "debit": 200}],
gle_filters={"account": "Cost of Goods Sold - TCP1"},
)
def test_return_with_lcv(self):
from erpnext.controllers.sales_and_purchase_return import make_return_doc
from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import (
create_landed_cost_voucher,
)
item = self.make_item().name
company = "_Test Company with perpetual inventory"
warehouse = "Stores - TCP1"
cost_center = "Main - TCP1"
pi = make_purchase_invoice(
item=item,
company=company,
warehouse=warehouse,
cost_center=cost_center,
update_stock=1,
qty=10,
rate=100,
)
# Create landed cost voucher - will increase valuation of received item by 10
create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100)
return_pi = make_return_doc(pi.doctype, pi.name)
return_pi.save().submit()
# assert that stock consumption is with actual in rate
self.assertGLEs(
return_pi,
[{"credit": 1100, "debit": 0}],
gle_filters={"account": "Stock In Hand - TCP1"},
)
# assert loss booked in COGS
self.assertGLEs(
return_pi,
[{"credit": 0, "debit": 100}],
gle_filters={"account": "Cost of Goods Sold - TCP1"},
)
def test_multi_currency_gle(self):
pi = make_purchase_invoice(
supplier="_Test Supplier USD",
@ -886,30 +930,6 @@ class TestPurchaseInvoice(unittest.TestCase):
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",
@ -1399,15 +1419,30 @@ class TestPurchaseInvoice(unittest.TestCase):
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,
@ -1423,7 +1458,7 @@ class TestPurchaseInvoice(unittest.TestCase):
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()
@ -1431,7 +1466,7 @@ class TestPurchaseInvoice(unittest.TestCase):
expected_gle = [
["Cash - _TC", 0, 27000],
["Creditors - _TC", 30000, 0],
["TDS Payable - _TC", 0, 3000],
[tds_account, 0, 3000],
]
gl_entries = frappe.db.sql(
@ -1457,7 +1492,7 @@ class TestPurchaseInvoice(unittest.TestCase):
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(
@ -1526,9 +1561,41 @@ class TestPurchaseInvoice(unittest.TestCase):
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
# Cancel purchase invoice to check reverse provisional entry cancellation
pi.cancel()
expected_gle_for_purchase_receipt_post_pi_cancel = [
["Provision Account - _TC", 0, 250, pi.posting_date],
["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date],
]
check_gl_entries(
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
)
company.enable_provisional_accounting_for_non_stock_items = 0
company.save()
def test_item_less_defaults(self):
pi = frappe.new_doc("Purchase Invoice")
pi.supplier = "_Test Supplier"
pi.company = "_Test Company"
pi.append(
"items",
{
"item_name": "Opening item",
"qty": 1,
"uom": "Tonne",
"stock_uom": "Kg",
"rate": 1000,
"expense_account": "Stock Received But Not Billed - _TC",
},
)
pi.save()
self.assertEqual(pi.items[0].conversion_factor, 1000)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql(
@ -1547,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",
{
"from_date": fiscal_year[1],
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 2500,
"cumulative_threshold": 0,
},
)
tds_category.save()
if not frappe.db.get_value(
"Tax Withholding Account", {"parent": "TDS - 194 - Dividends - Individual", "account": account}
):
tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual")
tds_category.append("accounts", {"company": company, "account": account})
tds_category.save()
"doctype": "Tax Withholding Category",
"name": category_name,
"category_name": category_name,
"accounts": [{"company": company, "account": account}],
"rates": [
{
"from_date": fiscal_year[1],
"to_date": fiscal_year[2],
"tax_withholding_rate": 10,
"single_threshold": 2500,
"cumulative_threshold": 0,
}
],
}
).insert(ignore_if_duplicate=True)
def unlink_payment_on_cancel_of_invoice(enable=1):

View File

@ -195,6 +195,7 @@
"label": "Rejected Qty"
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@ -214,6 +215,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
@ -222,6 +224,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Accepted Qty in Stock UOM",
@ -616,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",
@ -871,7 +877,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-11-15 17:04:07.191013",
"modified": "2022-06-17 05:31:10.520171",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@ -879,5 +885,6 @@
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -1,10 +1,12 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:08",
"description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
"title",
"is_default",
@ -74,7 +76,8 @@
],
"icon": "fa fa-money",
"idx": 1,
"modified": "2019-11-25 13:05:26.220275",
"links": [],
"modified": "2022-05-16 16:15:29.059370",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges Template",
@ -103,6 +106,10 @@
"role": "Purchase User"
}
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@ -1,38 +0,0 @@
{
"actions": [],
"creation": "2016-07-27 17:24:24.956896",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"account"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company"
},
{
"description": "Default Bank / Cash account will be automatically updated in Salary Journal Entry when this mode is selected.",
"fieldname": "account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Account",
"options": "Account"
}
],
"istable": 1,
"links": [],
"modified": "2020-10-18 17:57:57.110257",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Salary Component Account",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
}

View File

@ -1,9 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class SalaryComponentAccount(Document):
pass

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

@ -52,7 +52,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
me.frm.refresh_fields();
}
erpnext.queries.setup_warehouse_query(this.frm);
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
refresh(doc, dt, dn) {
@ -477,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
@ -784,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();
},
@ -861,27 +863,44 @@ frappe.ui.form.on('Sales Invoice', {
set_timesheet_data: function(frm, timesheets) {
frm.clear_table("timesheets")
timesheets.forEach(timesheet => {
timesheets.forEach(async (timesheet) => {
if (frm.doc.currency != timesheet.currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: timesheet.currency,
to_currency: frm.doc.currency
},
callback: function(r) {
if (r.message) {
exchange_rate = r.message;
frm.events.append_time_log(frm, timesheet, exchange_rate);
}
}
});
const exchange_rate = await frm.events.get_exchange_rate(
frm, timesheet.currency, frm.doc.currency
)
frm.events.append_time_log(frm, timesheet, exchange_rate)
} else {
frm.events.append_time_log(frm, timesheet, 1.0);
}
});
},
async get_exchange_rate(frm, from_currency, to_currency) {
if (
frm.exchange_rates
&& frm.exchange_rates[from_currency]
&& frm.exchange_rates[from_currency][to_currency]
) {
return frm.exchange_rates[from_currency][to_currency];
}
return frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency,
to_currency
},
callback: function(r) {
if (r.message) {
// cache exchange rates
frm.exchange_rates = frm.exchange_rates || {};
frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {};
frm.exchange_rates[from_currency][to_currency] = r.message;
}
}
});
},
append_time_log: function(frm, time_log, exchange_rate) {
const row = frm.add_child("timesheets");
row.activity_type = time_log.activity_type;
@ -892,7 +911,7 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_hours = time_log.billing_hours;
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name;
row.project_name = time_log.project_name;
row.project_name = time_log.project_name;
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");

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",
@ -1988,7 +1965,7 @@
{
"fieldname": "additional_discount_account",
"fieldtype": "Link",
"label": "Additional Discount Account",
"label": "Discount Account",
"options": "Account"
},
{
@ -2026,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",
@ -2038,7 +2022,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2022-03-08 16:08:53.517903",
"modified": "2022-06-16 16:22:44.870575",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -114,6 +114,7 @@ class SalesInvoice(SellingController):
self.set_income_account_for_fixed_assets()
self.validate_item_cost_centers()
self.validate_income_account()
self.check_conversion_rate()
validate_inter_company_party(
self.doctype, self.customer, self.company, self.inter_company_invoice_reference
@ -150,7 +151,6 @@ class SalesInvoice(SellingController):
)
self.set_against_income_account()
self.validate_c_form()
self.validate_time_sheets_are_submitted()
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
if not self.is_return:
@ -365,8 +365,6 @@ class SalesInvoice(SellingController):
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
self.update_serial_no(in_cancel=True)
self.validate_c_form_on_cancel()
# Updating stock ledger should always be called after updating prevdoc status,
# because updating reserved qty in bin depends upon updated delivered qty in SO
if self.update_stock == 1:
@ -396,7 +394,12 @@ class SalesInvoice(SellingController):
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
self.unlink_sales_invoice_from_timesheets()
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Repost Item Valuation",
"Payment Ledger Entry",
)
def update_status_updater_args(self):
if cint(self.update_stock):
@ -809,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:
@ -1024,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(
{
@ -1049,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")
@ -1506,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):
@ -2111,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)
@ -2175,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"]:
@ -2218,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()
@ -1583,6 +1630,17 @@ class TestSalesInvoice(unittest.TestCase):
self.assertTrue(gle)
def test_invoice_exchange_rate(self):
si = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=1,
do_not_save=1,
)
self.assertRaises(frappe.ValidationError, si.save)
def test_invalid_currency(self):
# Customer currency = USD
@ -1786,24 +1844,7 @@ class TestSalesInvoice(unittest.TestCase):
for i, k in enumerate(expected_values["keys"]):
self.assertEqual(d.get(k), expected_values[d.item_code][i])
def test_item_wise_tax_breakup_india(self):
frappe.flags.country = "India"
si = self.create_si_to_test_tax_breakup()
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
expected_itemised_tax = {
"_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}},
"_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}},
}
expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0}
self.assertEqual(itemised_tax, expected_itemised_tax)
self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount)
frappe.flags.country = None
def test_item_wise_tax_breakup_outside_india(self):
def test_item_wise_tax_breakup(self):
frappe.flags.country = "United States"
si = self.create_si_to_test_tax_breakup()
@ -1827,7 +1868,6 @@ class TestSalesInvoice(unittest.TestCase):
"items",
{
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": 100,
"rate": 50,
@ -1840,7 +1880,6 @@ class TestSalesInvoice(unittest.TestCase):
"items",
{
"item_code": "_Test Item 2",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": 100,
"rate": 50,
@ -1927,7 +1966,6 @@ class TestSalesInvoice(unittest.TestCase):
"items",
{
"item_code": "_Test Item",
"gst_hsn_code": "999800",
"warehouse": "_Test Warehouse - _TC",
"qty": 1,
"rate": rate,
@ -2600,77 +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
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")
@ -3139,6 +3106,40 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertTrue(si.items[0].serial_no)
def test_sales_invoice_with_disabled_account(self):
try:
account_name = "Sales Expenses - _TC"
account = frappe.get_doc("Account", account_name)
account.disabled = 1
account.save()
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.taxes = []
si.append(
"taxes",
{
"charge_type": "On Net Total",
"account_head": account_name,
"cost_center": "Main - _TC",
"description": "Commission",
"rate": 5,
},
)
si.save()
with self.assertRaises(frappe.ValidationError) as err:
si.submit()
self.assertTrue(
"Cannot create accounting entries against disabled accounts" in str(err.exception)
)
finally:
account.disabled = 0
account.save()
def test_gain_loss_with_advance_entry(self):
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
@ -3231,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
@ -3444,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",
@ -3497,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

@ -182,6 +182,7 @@
"oldfieldtype": "Currency"
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
@ -200,6 +201,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
@ -207,6 +209,7 @@
"reqd": 1
},
{
"depends_on": "eval:doc.uom != doc.stock_uom",
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Qty as per Stock UOM",
@ -843,7 +846,7 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2022-03-23 08:18:04.928287",
"modified": "2022-06-17 05:33:15.335912",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2013-01-10 16:34:09",
@ -77,7 +78,8 @@
],
"icon": "fa fa-money",
"idx": 1,
"modified": "2019-11-25 13:06:03.279099",
"links": [],
"modified": "2022-05-16 16:14:52.061672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges Template",
@ -113,7 +115,10 @@
"write": 1
}
],
"show_title_field_in_link": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"title_field": "title",
"track_changes": 1
}

View File

@ -145,13 +145,14 @@ class Subscription(Document):
You shouldn't need to call this directly. Use `get_billing_cycle` instead.
"""
plan_names = [plan.plan for plan in self.plans]
billing_info = frappe.db.sql(
"select distinct `billing_interval`, `billing_interval_count` "
"from `tabSubscription Plan` "
"where name in %s",
(plan_names,),
as_dict=1,
)
subscription_plan = frappe.qb.DocType("Subscription Plan")
billing_info = (
frappe.qb.from_(subscription_plan)
.select(subscription_plan.billing_interval, subscription_plan.billing_interval_count)
.distinct()
.where(subscription_plan.name.isin(plan_names))
).run(as_dict=1)
return billing_info

View File

@ -163,17 +163,15 @@ def get_party_details(party, party_type, args=None):
def get_tax_template(posting_date, args):
"""Get matching tax rule"""
args = frappe._dict(args)
from_date = to_date = posting_date
if not posting_date:
from_date = "1900-01-01"
to_date = "4000-01-01"
conditions = []
conditions = [
"""(from_date is null or from_date <= '{0}')
and (to_date is null or to_date >= '{1}')""".format(
from_date, to_date
if posting_date:
conditions.append(
f"""(from_date is null or from_date <= '{posting_date}')
and (to_date is null or to_date >= '{posting_date}')"""
)
]
else:
conditions.append("(from_date is null) and (to_date is null)")
conditions.append(
"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))

View File

@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry
class ClosedAccountingPeriod(frappe.ValidationError):
@ -31,8 +32,16 @@ def make_gl_entries(
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
validate_disabled_accounts(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
create_payment_ledger_entry(
gl_map,
cancel=0,
adv_adj=adv_adj,
update_outstanding=update_outstanding,
from_repost=from_repost,
)
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries
elif gl_map:
@ -45,6 +54,26 @@ def make_gl_entries(
make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding)
def validate_disabled_accounts(gl_map):
accounts = [d.account for d in gl_map if d.account]
Account = frappe.qb.DocType("Account")
disabled_accounts = (
frappe.qb.from_(Account)
.where(Account.name.isin(accounts) & Account.disabled == 1)
.select(Account.name, Account.disabled)
).run(as_dict=True)
if disabled_accounts:
account_list = "<br>"
account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts])
frappe.throw(
_("Cannot create accounting entries against disabled accounts: {0}").format(account_list),
title=_("Disabled Account Selected"),
)
def validate_accounting_period(gl_map):
accounting_periods = frappe.db.sql(
""" SELECT
@ -103,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:
@ -139,6 +168,7 @@ def get_cost_center_allocation_data(company, posting_date):
def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
for entry in gl_map:
# if there is already an entry in this account then just add it
# to that entry
@ -269,9 +299,10 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle.flags.from_repost = from_repost
gle.flags.adv_adj = adv_adj
gle.flags.update_outstanding = update_outstanding or "Yes"
gle.flags.notify_update = False
gle.submit()
if not from_repost:
if not from_repost and gle.voucher_type != "Period Closing Voucher":
validate_expense_against_budget(args)
@ -458,6 +489,10 @@ def make_reverse_gl_entries(
).run(as_dict=1)
if gl_entries:
create_payment_ledger_entry(gl_entries, cancel=1)
create_payment_ledger_entry(
gl_entries, cancel=1, adv_adj=adv_adj, update_outstanding=update_outstanding
)
validate_accounting_period(gl_entries)
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])

View File

@ -13,7 +13,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
"idx": 0,
"is_complete": 0,
"modified": "2022-01-18 18:35:52.326688",
"modified": "2022-06-14 17:38:24.967834",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts",

View File

@ -1,8 +1,8 @@
{
"action": "Watch Video",
"action": "Go to Page",
"action_label": "Learn more about Chart of Accounts",
"callback_message": "You can continue with the onboarding after exploring this page",
"callback_title": "Awesome Work",
"callback_title": "Explore Chart of Accounts",
"creation": "2020-05-13 19:58:20.928127",
"description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
"docstatus": 0,
@ -12,7 +12,7 @@
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:46:25.878506",
"modified": "2022-06-07 14:21:26.264769",
"modified_by": "Administrator",
"name": "Chart of Accounts",
"owner": "Administrator",

View File

@ -2,14 +2,14 @@
"action": "Create Entry",
"action_label": "Manage Sales Tax Templates",
"creation": "2020-05-13 19:29:43.844463",
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n",
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
"is_single": 0,
"is_skipped": 0,
"modified": "2021-08-13 11:48:37.238610",
"modified": "2022-06-14 17:37:56.694261",
"modified_by": "Administrator",
"name": "Setup Taxes",
"owner": "Administrator",

View File

@ -211,7 +211,7 @@ def set_address_details(
else:
party_details.update(get_company_address(company))
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order"]:
if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]:
if party_details.company_address:
party_details.update(
get_fetch_values(doctype, "company_address", party_details.company_address)
@ -897,3 +897,18 @@ def get_default_contact(doctype, name):
return None
else:
return None
def add_party_account(party_type, party, company, account):
doc = frappe.get_doc(party_type, party)
account_exists = False
for d in doc.get("accounts"):
if d.account == account:
account_exists = True
if not account_exists:
accounts = {"company": company, "account": account}
doc.append("accounts", accounts)
doc.save()

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