Merge branch 'develop' into perf-bom-update-tool
This commit is contained in:
commit
ff0a6b7cbd
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@ -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
12
.github/stale.yml
vendored
@ -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: 90
|
||||
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
|
||||
|
11
cypress.json
11
cypress.json
@ -1,11 +0,0 @@
|
||||
{
|
||||
"baseUrl": "http://test_site:8000/",
|
||||
"projectId": "da59y9",
|
||||
"adminPassword": "admin",
|
||||
"defaultCommandTimeout": 20000,
|
||||
"pageLoadTimeout": 15000,
|
||||
"retries": {
|
||||
"runMode": 2,
|
||||
"openMode": 2
|
||||
}
|
||||
}
|
@ -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"
|
||||
}
|
@ -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");
|
||||
});
|
||||
});
|
@ -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');
|
||||
});
|
||||
});
|
@ -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");
|
||||
});
|
||||
});
|
@ -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]}`);
|
||||
});
|
||||
});
|
||||
});
|
@ -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]}`);
|
||||
});
|
||||
});
|
||||
});
|
@ -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
|
||||
};
|
@ -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)}`);
|
||||
});
|
@ -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'
|
||||
});
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"allowJs": true,
|
||||
"baseUrl": "../node_modules",
|
||||
"types": [
|
||||
"cypress"
|
||||
]
|
||||
},
|
||||
"include": [
|
||||
"**/*.*"
|
||||
]
|
||||
}
|
@ -94,7 +94,7 @@ class JournalEntry(AccountsController):
|
||||
|
||||
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()
|
||||
|
@ -95,7 +95,7 @@ class PaymentEntry(AccountsController):
|
||||
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()
|
||||
|
@ -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) {
|
||||
|
||||
// }
|
||||
});
|
@ -0,0 +1,180 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"autoname": "format:PLE-{YY}-{MM}-{######}",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"fieldname": "party_type",
|
||||
"fieldtype": "Link",
|
||||
"label": "Party Type",
|
||||
"options": "DocType"
|
||||
},
|
||||
{
|
||||
"fieldname": "party",
|
||||
"fieldtype": "Dynamic Link",
|
||||
"label": "Party",
|
||||
"options": "party_type"
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"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-05-19 18:04:44.609115",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Ledger Entry",
|
||||
"naming_rule": "Expression",
|
||||
"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": []
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
# 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
|
||||
|
||||
|
||||
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(self):
|
||||
self.validate_account()
|
@ -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])
|
@ -78,6 +78,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,6 +88,8 @@ 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)
|
||||
@ -119,14 +123,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",
|
||||
|
@ -102,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);
|
||||
@ -112,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();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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) {
|
||||
|
@ -545,7 +545,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 +1136,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)
|
||||
):
|
||||
@ -1418,7 +1427,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):
|
||||
|
@ -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()
|
||||
@ -693,6 +694,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",
|
||||
@ -1526,6 +1601,18 @@ 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()
|
||||
|
||||
|
@ -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
|
||||
}
|
@ -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) {
|
||||
@ -861,27 +860,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 +908,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");
|
||||
|
@ -396,7 +396,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):
|
||||
|
@ -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
|
||||
}
|
@ -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):
|
||||
@ -34,6 +35,7 @@ def make_gl_entries(
|
||||
validate_disabled_accounts(gl_map)
|
||||
gl_map = process_gl_map(gl_map, merge_entries)
|
||||
if gl_map and len(gl_map) > 1:
|
||||
create_payment_ledger_entry(gl_map)
|
||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||
# Post GL Map proccess there may no be any GL Entries
|
||||
elif gl_map:
|
||||
@ -479,6 +481,7 @@ def make_reverse_gl_entries(
|
||||
).run(as_dict=1)
|
||||
|
||||
if gl_entries:
|
||||
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||
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"])
|
||||
|
@ -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()
|
||||
|
@ -198,10 +198,12 @@ def get_loan_entries(filters):
|
||||
amount_field = (loan_doc.disbursed_amount).as_("credit")
|
||||
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
||||
account = loan_doc.disbursement_account
|
||||
salary_condition = loan_doc.docstatus == 1
|
||||
else:
|
||||
amount_field = (loan_doc.amount_paid).as_("debit")
|
||||
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||
account = loan_doc.payment_account
|
||||
salary_condition = loan_doc.repay_from_salary == 0
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(loan_doc)
|
||||
@ -214,14 +216,12 @@ def get_loan_entries(filters):
|
||||
posting_date,
|
||||
)
|
||||
.where(loan_doc.docstatus == 1)
|
||||
.where(salary_condition)
|
||||
.where(account == filters.get("account"))
|
||||
.where(posting_date <= getdate(filters.get("report_date")))
|
||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date")))
|
||||
)
|
||||
|
||||
if doctype == "Loan Repayment":
|
||||
query.where(loan_doc.repay_from_salary == 0)
|
||||
|
||||
entries = query.run(as_dict=1)
|
||||
loan_docs.extend(entries)
|
||||
|
||||
@ -267,15 +267,17 @@ def get_loan_amount(filters):
|
||||
amount_field = Sum(loan_doc.disbursed_amount)
|
||||
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
||||
account = loan_doc.disbursement_account
|
||||
salary_condition = loan_doc.docstatus == 1
|
||||
else:
|
||||
amount_field = Sum(loan_doc.amount_paid)
|
||||
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||
account = loan_doc.payment_account
|
||||
|
||||
salary_condition = loan_doc.repay_from_salary == 0
|
||||
amount = (
|
||||
frappe.qb.from_(loan_doc)
|
||||
.select(amount_field)
|
||||
.where(loan_doc.docstatus == 1)
|
||||
.where(salary_condition)
|
||||
.where(account == filters.get("account"))
|
||||
.where(posting_date > getdate(filters.get("report_date")))
|
||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date")))
|
||||
|
@ -262,7 +262,10 @@ def get_report_summary(summary_data, currency):
|
||||
def get_chart_data(columns, data):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
datasets = [
|
||||
{"name": account.get("account").replace("'", ""), "values": [account.get("total")]}
|
||||
{
|
||||
"name": account.get("account").replace("'", ""),
|
||||
"values": [account.get(d.get("fieldname")) for d in columns[2:]],
|
||||
}
|
||||
for account in data
|
||||
if account.get("parent_account") == None and account.get("currency")
|
||||
]
|
||||
|
@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
|
||||
] # nosec
|
||||
|
||||
|
||||
def get_deducted_taxes():
|
||||
return frappe.db.sql_list(
|
||||
"select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'"
|
||||
)
|
||||
|
||||
|
||||
def get_tax_accounts(
|
||||
item_list,
|
||||
columns,
|
||||
@ -462,6 +456,7 @@ def get_tax_accounts(
|
||||
tax_columns = []
|
||||
invoice_item_row = {}
|
||||
itemised_tax = {}
|
||||
add_deduct_tax = "charge_type"
|
||||
|
||||
tax_amount_precision = (
|
||||
get_field_precision(
|
||||
@ -477,13 +472,13 @@ def get_tax_accounts(
|
||||
conditions = ""
|
||||
if doctype == "Purchase Invoice":
|
||||
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
||||
add_deduct_tax = "add_deduct_tax"
|
||||
|
||||
deducted_tax = get_deducted_taxes()
|
||||
tax_details = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
name, parent, description, item_wise_tax_detail,
|
||||
charge_type, base_tax_amount_after_discount_amount
|
||||
charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount
|
||||
from `tab%s`
|
||||
where
|
||||
parenttype = %s and docstatus = 1
|
||||
@ -491,12 +486,22 @@ def get_tax_accounts(
|
||||
and parent in (%s)
|
||||
%s
|
||||
order by description
|
||||
"""
|
||||
""".format(
|
||||
add_deduct_tax=add_deduct_tax
|
||||
)
|
||||
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
||||
tuple([doctype] + list(invoice_item_row)),
|
||||
)
|
||||
|
||||
for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details:
|
||||
for (
|
||||
name,
|
||||
parent,
|
||||
description,
|
||||
item_wise_tax_detail,
|
||||
charge_type,
|
||||
add_deduct_tax,
|
||||
tax_amount,
|
||||
) in tax_details:
|
||||
description = handle_html(description)
|
||||
if description not in tax_columns and tax_amount:
|
||||
# as description is text editor earlier and markup can break the column convention in reports
|
||||
@ -529,7 +534,9 @@ def get_tax_accounts(
|
||||
if item_tax_amount:
|
||||
tax_value = flt(item_tax_amount, tax_amount_precision)
|
||||
tax_value = (
|
||||
tax_value * -1 if (doctype == "Purchase Invoice" and name in deducted_tax) else tax_value
|
||||
tax_value * -1
|
||||
if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct")
|
||||
else tax_value
|
||||
)
|
||||
|
||||
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||
|
@ -211,6 +211,7 @@ def set_gl_entries_by_account(
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and {based_on} is not null
|
||||
and is_cancelled = 0
|
||||
order by {based_on}, posting_date""".format(
|
||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||
),
|
||||
|
@ -346,9 +346,13 @@ def get_columns(invoice_list, additional_table_columns):
|
||||
def get_conditions(filters):
|
||||
conditions = ""
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False) or []
|
||||
accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
|
||||
|
||||
if filters.get("company"):
|
||||
conditions += " and company=%(company)s"
|
||||
if filters.get("customer"):
|
||||
|
||||
if filters.get("customer") and "customer" not in accounting_dimensions_list:
|
||||
conditions += " and customer = %(customer)s"
|
||||
|
||||
if filters.get("from_date"):
|
||||
@ -359,32 +363,18 @@ def get_conditions(filters):
|
||||
if filters.get("owner"):
|
||||
conditions += " and owner = %(owner)s"
|
||||
|
||||
if filters.get("mode_of_payment"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
||||
def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
|
||||
if not filters.get(field) or field in accounting_dimensions_list:
|
||||
return ""
|
||||
return f""" and exists(select name from `tab{table}`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
||||
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||
|
||||
if filters.get("cost_center"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
|
||||
|
||||
if filters.get("warehouse"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s)"""
|
||||
|
||||
if filters.get("brand"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s)"""
|
||||
|
||||
if filters.get("item_group"):
|
||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
||||
where parent=`tabSales Invoice`.name
|
||||
and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s)"""
|
||||
|
||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||
conditions += get_sales_invoice_item_field_condition("brand")
|
||||
conditions += get_sales_invoice_item_field_condition("item_group")
|
||||
|
||||
if accounting_dimensions:
|
||||
common_condition = """
|
||||
|
@ -7,7 +7,7 @@ from typing import List, Tuple
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, throw
|
||||
from frappe import _, qb, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||
|
||||
@ -15,6 +15,7 @@ import erpnext
|
||||
|
||||
# imported to enable erpnext.accounts.utils.get_account_currency
|
||||
from erpnext.accounts.doctype.account.account import get_account_currency # noqa
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_dimensions
|
||||
from erpnext.stock import get_warehouse_account_map
|
||||
from erpnext.stock.utils import get_stock_value_on
|
||||
|
||||
@ -1345,3 +1346,102 @@ def check_and_delete_linked_reports(report):
|
||||
if icons:
|
||||
for icon in icons:
|
||||
frappe.delete_doc("Desktop Icon", icon)
|
||||
|
||||
|
||||
def create_payment_ledger_entry(gl_entries, cancel=0):
|
||||
if gl_entries:
|
||||
ple = None
|
||||
|
||||
# companies
|
||||
account = qb.DocType("Account")
|
||||
companies = list(set([x.company for x in gl_entries]))
|
||||
|
||||
# receivable/payable account
|
||||
accounts_with_types = (
|
||||
qb.from_(account)
|
||||
.select(account.name, account.account_type)
|
||||
.where(
|
||||
(account.account_type.isin(["Receivable", "Payable"]) & (account.company.isin(companies)))
|
||||
)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
receivable_or_payable_accounts = [y.name for y in accounts_with_types]
|
||||
|
||||
def get_account_type(account):
|
||||
for entry in accounts_with_types:
|
||||
if entry.name == account:
|
||||
return entry.account_type
|
||||
|
||||
dr_or_cr = 0
|
||||
account_type = None
|
||||
for gle in gl_entries:
|
||||
if gle.account in receivable_or_payable_accounts:
|
||||
account_type = get_account_type(gle.account)
|
||||
if account_type == "Receivable":
|
||||
dr_or_cr = gle.debit - gle.credit
|
||||
dr_or_cr_account_currency = gle.debit_in_account_currency - gle.credit_in_account_currency
|
||||
elif account_type == "Payable":
|
||||
dr_or_cr = gle.credit - gle.debit
|
||||
dr_or_cr_account_currency = gle.credit_in_account_currency - gle.debit_in_account_currency
|
||||
|
||||
if cancel:
|
||||
dr_or_cr *= -1
|
||||
dr_or_cr_account_currency *= -1
|
||||
|
||||
ple = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Payment Ledger Entry",
|
||||
"posting_date": gle.posting_date,
|
||||
"company": gle.company,
|
||||
"account_type": account_type,
|
||||
"account": gle.account,
|
||||
"party_type": gle.party_type,
|
||||
"party": gle.party,
|
||||
"cost_center": gle.cost_center,
|
||||
"finance_book": gle.finance_book,
|
||||
"due_date": gle.due_date,
|
||||
"voucher_type": gle.voucher_type,
|
||||
"voucher_no": gle.voucher_no,
|
||||
"against_voucher_type": gle.against_voucher_type
|
||||
if gle.against_voucher_type
|
||||
else gle.voucher_type,
|
||||
"against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no,
|
||||
"currency": gle.currency,
|
||||
"amount": dr_or_cr,
|
||||
"amount_in_account_currency": dr_or_cr_account_currency,
|
||||
"delinked": True if cancel else False,
|
||||
}
|
||||
)
|
||||
|
||||
dimensions_and_defaults = get_dimensions()
|
||||
if dimensions_and_defaults:
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
ple.set(dimension.fieldname, gle.get(dimension.fieldname))
|
||||
|
||||
if cancel:
|
||||
delink_original_entry(ple)
|
||||
ple.flags.ignore_permissions = 1
|
||||
ple.submit()
|
||||
|
||||
|
||||
def delink_original_entry(pl_entry):
|
||||
if pl_entry:
|
||||
ple = qb.DocType("Payment Ledger Entry")
|
||||
query = (
|
||||
qb.update(ple)
|
||||
.set(ple.delinked, True)
|
||||
.set(ple.modified, now())
|
||||
.set(ple.modified_by, frappe.session.user)
|
||||
.where(
|
||||
(ple.company == pl_entry.company)
|
||||
& (ple.account_type == pl_entry.account_type)
|
||||
& (ple.account == pl_entry.account)
|
||||
& (ple.party_type == pl_entry.party_type)
|
||||
& (ple.party == pl_entry.party)
|
||||
& (ple.voucher_type == pl_entry.voucher_type)
|
||||
& (ple.voucher_no == pl_entry.voucher_no)
|
||||
& (ple.against_voucher_type == pl_entry.against_voucher_type)
|
||||
& (ple.against_voucher_no == pl_entry.against_voucher_no)
|
||||
)
|
||||
)
|
||||
query.run()
|
||||
|
@ -148,7 +148,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-14 15:56:42.340223",
|
||||
"modified": "2022-05-31 19:40:26.103909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -162,6 +162,16 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
@ -18,7 +18,7 @@ class BuyingSettings(Document):
|
||||
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
|
||||
frappe.db.set_default(key, self.get(key, ""))
|
||||
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
from erpnext.utilities.naming import set_by_naming_series
|
||||
|
||||
set_by_naming_series(
|
||||
"Supplier",
|
||||
|
@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", {
|
||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||
return erpnext.queries.warehouse(frm.doc);
|
||||
});
|
||||
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
||||
},
|
||||
|
||||
apply_tds: function(frm) {
|
||||
|
@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
|
||||
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
||||
|
||||
def on_cancel(self):
|
||||
self.ignore_linked_doctypes = "Payment Ledger Entry"
|
||||
super(PurchaseOrder, self).on_cancel()
|
||||
|
||||
if self.is_against_so():
|
||||
|
@ -34,6 +34,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
||||
from erpnext.accounts.party import (
|
||||
get_party_account,
|
||||
get_party_account_currency,
|
||||
get_party_gle_currency,
|
||||
validate_party_frozen_disabled,
|
||||
)
|
||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
||||
@ -168,6 +169,7 @@ class AccountsController(TransactionBase):
|
||||
|
||||
self.validate_party()
|
||||
self.validate_currency()
|
||||
self.validate_party_account_currency()
|
||||
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||
@ -1447,6 +1449,27 @@ class AccountsController(TransactionBase):
|
||||
# at quotation / sales order level and we shouldn't stop someone
|
||||
# from creating a sales invoice if sales order is already created
|
||||
|
||||
def validate_party_account_currency(self):
|
||||
if self.doctype not in ("Sales Invoice", "Purchase Invoice"):
|
||||
return
|
||||
|
||||
if self.is_opening == "Yes":
|
||||
return
|
||||
|
||||
party_type, party = self.get_party()
|
||||
party_gle_currency = get_party_gle_currency(party_type, party, self.company)
|
||||
party_account = (
|
||||
self.get("debit_to") if self.doctype == "Sales Invoice" else self.get("credit_to")
|
||||
)
|
||||
party_account_currency = get_account_currency(party_account)
|
||||
|
||||
if not party_gle_currency and (party_account_currency != self.currency):
|
||||
frappe.throw(
|
||||
_("Party Account {0} currency ({1}) and document currency ({2}) should be same").format(
|
||||
frappe.bold(party_account), party_account_currency, self.currency
|
||||
)
|
||||
)
|
||||
|
||||
def delink_advance_entries(self, linked_doc_name):
|
||||
total_allocated_amount = 0
|
||||
for adv in self.advances:
|
||||
@ -1843,7 +1866,7 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non
|
||||
def get_taxes_and_charges(master_doctype, master_name):
|
||||
if not master_name:
|
||||
return
|
||||
from frappe.model import default_fields
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
|
||||
tax_master = frappe.get_doc(master_doctype, master_name)
|
||||
|
||||
@ -1851,7 +1874,7 @@ def get_taxes_and_charges(master_doctype, master_name):
|
||||
for i, tax in enumerate(tax_master.get("taxes")):
|
||||
tax = tax.as_dict()
|
||||
|
||||
for fieldname in default_fields:
|
||||
for fieldname in default_fields + child_table_fields:
|
||||
if fieldname in tax:
|
||||
del tax[fieldname]
|
||||
|
||||
@ -2638,7 +2661,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_reserved_qty_for_subcontract()
|
||||
parent.create_raw_materials_supplied("supplied_items")
|
||||
parent.save()
|
||||
else:
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
parent.update_project()
|
||||
parent.update_prevdoc_status("submit")
|
||||
|
@ -316,7 +316,7 @@ def get_returned_qty_map_for_row(return_against, party, row_name, doctype):
|
||||
return data[0]
|
||||
|
||||
|
||||
def make_return_doc(doctype, source_name, target_doc=None):
|
||||
def make_return_doc(doctype: str, source_name: str, target_doc=None):
|
||||
from frappe.model.mapper import get_mapped_doc
|
||||
|
||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||
|
@ -34,9 +34,7 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
def autoname(self):
|
||||
# use naming series to accomodate items with same name (different item code)
|
||||
from frappe.model.naming import make_autoname
|
||||
|
||||
from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
|
||||
from frappe.model.naming import get_default_naming_series, make_autoname
|
||||
|
||||
naming_series = get_default_naming_series("Website Item")
|
||||
if not self.name and naming_series:
|
||||
|
@ -490,6 +490,7 @@ communication_doctypes = ["Customer", "Supplier"]
|
||||
|
||||
accounting_dimension_doctypes = [
|
||||
"GL Entry",
|
||||
"Payment Ledger Entry",
|
||||
"Sales Invoice",
|
||||
"Purchase Invoice",
|
||||
"Payment Entry",
|
||||
|
@ -3,7 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
get_last_day,
|
||||
get_year_ending,
|
||||
get_year_start,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.hr.doctype.attendance.attendance import (
|
||||
DuplicateAttendanceError,
|
||||
@ -138,69 +146,70 @@ class TestAttendance(FrappeTestCase):
|
||||
self.assertEqual(attendance, fetch_attendance)
|
||||
|
||||
def test_unmarked_days(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
attendance_date = add_days(first_sunday, 1)
|
||||
|
||||
employee = make_employee(
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||
)
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
|
||||
mark_attendance(employee, first_day, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||
# holiday considered in unmarked days
|
||||
self.assertIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_excluding_holidays(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
attendance_date = add_days(first_sunday, 1)
|
||||
|
||||
employee = make_employee(
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||
)
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
|
||||
mark_attendance(employee, first_day, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||
# holidays not considered in unmarked days
|
||||
self.assertNotIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
date = add_days(first_sunday, 1)
|
||||
|
||||
doj = add_days(first_day, 1)
|
||||
relieving_date = add_days(first_day, 5)
|
||||
doj = add_days(date, 1)
|
||||
relieving_date = add_days(date, 5)
|
||||
employee = make_employee(
|
||||
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
|
||||
)
|
||||
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
attendance_date = add_days(first_day, 2)
|
||||
attendance_date = add_days(date, 2)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
persons_name = anniversary_person
|
||||
# Number of years completed at the company
|
||||
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
|
||||
anniversary_person += f" completed {completed_years} year(s)"
|
||||
anniversary_person += f" completed {get_pluralized_years(completed_years)}"
|
||||
else:
|
||||
person_names_with_years = []
|
||||
names = []
|
||||
@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
names.append(person_text)
|
||||
# Number of years completed at the company
|
||||
completed_years = getdate().year - person["date_of_joining"].year
|
||||
person_text += f" completed {completed_years} year(s)"
|
||||
person_text += f" completed {get_pluralized_years(completed_years)}"
|
||||
person_names_with_years.append(person_text)
|
||||
|
||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
||||
@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
return reminder_text, message
|
||||
|
||||
|
||||
def get_pluralized_years(years):
|
||||
if years == 1:
|
||||
return "1 year"
|
||||
return f"{years} years"
|
||||
|
||||
|
||||
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
|
@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController):
|
||||
|
||||
def on_cancel(self):
|
||||
self.update_task_and_project()
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
|
||||
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
|
||||
if self.payable_account:
|
||||
self.make_gl_entries(cancel=True)
|
||||
|
||||
|
@ -22,7 +22,7 @@ class HRSettings(Document):
|
||||
PROCEED_WITH_FREQUENCY_CHANGE = False
|
||||
|
||||
def set_naming_series(self):
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
from erpnext.utilities.naming import set_by_naming_series
|
||||
|
||||
set_by_naming_series(
|
||||
"Employee",
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import get_link_to_form
|
||||
from frappe.website.website_generator import WebsiteGenerator
|
||||
|
||||
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
|
||||
@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator):
|
||||
self.staffing_plan = staffing_plan[0].name
|
||||
self.planned_vacancies = staffing_plan[0].vacancies
|
||||
elif not self.planned_vacancies:
|
||||
planned_vacancies = frappe.db.sql(
|
||||
"""
|
||||
select vacancies from `tabStaffing Plan Detail`
|
||||
where parent=%s and designation=%s""",
|
||||
(self.staffing_plan, self.designation),
|
||||
self.planned_vacancies = frappe.db.get_value(
|
||||
"Staffing Plan Detail",
|
||||
{"parent": self.staffing_plan, "designation": self.designation},
|
||||
"vacancies",
|
||||
)
|
||||
self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None
|
||||
|
||||
if self.staffing_plan and self.planned_vacancies:
|
||||
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
|
||||
lft, rgt = frappe.get_cached_value("Company", staffing_plan_company, ["lft", "rgt"])
|
||||
|
||||
designation_counts = get_designation_counts(self.designation, self.company)
|
||||
designation_counts = get_designation_counts(self.designation, self.company, self.name)
|
||||
current_count = designation_counts["employee_count"] + designation_counts["job_openings"]
|
||||
|
||||
if self.planned_vacancies <= current_count:
|
||||
number_of_positions = frappe.db.get_value(
|
||||
"Staffing Plan Detail",
|
||||
{"parent": self.staffing_plan, "designation": self.designation},
|
||||
"number_of_positions",
|
||||
)
|
||||
|
||||
if number_of_positions <= current_count:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
|
||||
).format(self.designation, self.staffing_plan)
|
||||
"Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}"
|
||||
).format(
|
||||
frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan)
|
||||
),
|
||||
title=_("Vacancies fulfilled"),
|
||||
)
|
||||
|
||||
def get_context(self, context):
|
||||
|
@ -3,8 +3,77 @@
|
||||
|
||||
import unittest
|
||||
|
||||
# test_records = frappe.get_test_records('Job Opening')
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.staffing_plan.test_staffing_plan import make_company
|
||||
|
||||
|
||||
class TestJobOpening(unittest.TestCase):
|
||||
pass
|
||||
class TestJobOpening(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Staffing Plan")
|
||||
frappe.db.delete("Staffing Plan Detail")
|
||||
frappe.db.delete("Job Opening")
|
||||
|
||||
make_company("_Test Opening Company", "_TOC")
|
||||
frappe.db.delete("Employee", {"company": "_Test Opening Company"})
|
||||
|
||||
def test_vacancies_fulfilled(self):
|
||||
make_employee(
|
||||
"test_job_opening@example.com", company="_Test Opening Company", designation="Designer"
|
||||
)
|
||||
|
||||
staffing_plan = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Staffing Plan",
|
||||
"company": "_Test Opening Company",
|
||||
"name": "Test",
|
||||
"from_date": getdate(),
|
||||
"to_date": add_days(getdate(), 10),
|
||||
}
|
||||
)
|
||||
|
||||
staffing_plan.append(
|
||||
"staffing_details",
|
||||
{"designation": "Designer", "vacancies": 1, "estimated_cost_per_position": 50000},
|
||||
)
|
||||
staffing_plan.insert()
|
||||
staffing_plan.submit()
|
||||
|
||||
self.assertEqual(staffing_plan.staffing_details[0].number_of_positions, 2)
|
||||
|
||||
# allows creating 1 job opening as per vacancy
|
||||
opening_1 = get_job_opening()
|
||||
opening_1.insert()
|
||||
|
||||
# vacancies as per staffing plan already fulfilled via job opening and existing employee count
|
||||
opening_2 = get_job_opening(job_title="Designer New")
|
||||
self.assertRaises(frappe.ValidationError, opening_2.insert)
|
||||
|
||||
# allows updating existing job opening
|
||||
opening_1.status = "Closed"
|
||||
opening_1.save()
|
||||
|
||||
|
||||
def get_job_opening(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
opening = frappe.db.exists("Job Opening", {"job_title": args.job_title or "Designer"})
|
||||
if opening:
|
||||
return frappe.get_doc("Job Opening", opening)
|
||||
|
||||
opening = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Job Opening",
|
||||
"job_title": "Designer",
|
||||
"designation": "Designer",
|
||||
"company": "_Test Opening Company",
|
||||
"status": "Open",
|
||||
}
|
||||
)
|
||||
|
||||
opening.update(args)
|
||||
|
||||
return opening
|
||||
|
@ -173,7 +173,7 @@ frappe.ui.form.on("Leave Application", {
|
||||
date: frm.doc.from_date,
|
||||
to_date: frm.doc.to_date,
|
||||
leave_type: frm.doc.leave_type,
|
||||
consider_all_leaves_in_the_allocation_period: true
|
||||
consider_all_leaves_in_the_allocation_period: 1
|
||||
},
|
||||
callback: function (r) {
|
||||
if (!r.exc && r.message) {
|
||||
|
@ -88,7 +88,7 @@ class LeaveApplication(Document):
|
||||
share_doc_with_approver(self, self.leave_approver)
|
||||
|
||||
def on_submit(self):
|
||||
if self.status == "Open":
|
||||
if self.status in ["Open", "Cancelled"]:
|
||||
frappe.throw(
|
||||
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
||||
)
|
||||
@ -757,22 +757,6 @@ def get_leave_details(employee, date):
|
||||
leave_allocation = {}
|
||||
for d in allocation_records:
|
||||
allocation = allocation_records.get(d, frappe._dict())
|
||||
|
||||
total_allocated_leaves = (
|
||||
frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{
|
||||
"from_date": ("<=", date),
|
||||
"to_date": (">=", date),
|
||||
"employee": employee,
|
||||
"leave_type": allocation.leave_type,
|
||||
"docstatus": 1,
|
||||
},
|
||||
"SUM(total_leaves_allocated)",
|
||||
)
|
||||
or 0
|
||||
)
|
||||
|
||||
remaining_leaves = get_leave_balance_on(
|
||||
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
||||
)
|
||||
@ -782,10 +766,11 @@ def get_leave_details(employee, date):
|
||||
leaves_pending = get_leaves_pending_approval_for_period(
|
||||
employee, d, allocation.from_date, end_date
|
||||
)
|
||||
expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
|
||||
|
||||
leave_allocation[d] = {
|
||||
"total_leaves": total_allocated_leaves,
|
||||
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
|
||||
"total_leaves": allocation.total_leaves_allocated,
|
||||
"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
|
||||
"leaves_taken": leaves_taken,
|
||||
"leaves_pending_approval": leaves_pending,
|
||||
"remaining_leaves": remaining_leaves,
|
||||
@ -830,7 +815,7 @@ def get_leave_balance_on(
|
||||
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
||||
allocation = allocation_records.get(leave_type, frappe._dict())
|
||||
|
||||
end_date = allocation.to_date if consider_all_leaves_in_the_allocation_period else date
|
||||
end_date = allocation.to_date if cint(consider_all_leaves_in_the_allocation_period) else date
|
||||
cf_expiry = get_allocation_expiry_for_cf_leaves(employee, leave_type, to_date, date)
|
||||
|
||||
leaves_taken = get_leaves_for_period(employee, leave_type, allocation.from_date, end_date)
|
||||
@ -1117,7 +1102,7 @@ def add_leaves(events, start, end, filter_conditions=None):
|
||||
WHERE
|
||||
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
||||
AND docstatus < 2
|
||||
AND status != 'Rejected'
|
||||
AND status in ('Approved', 'Open')
|
||||
"""
|
||||
|
||||
if conditions:
|
||||
@ -1201,24 +1186,33 @@ def get_mandatory_approval(doctype):
|
||||
|
||||
|
||||
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||
query = """
|
||||
select employee, leave_type, from_date, to_date, total_leave_days
|
||||
from `tabLeave Application`
|
||||
where employee=%(employee)s
|
||||
and docstatus=1
|
||||
and (from_date between %(from_date)s and %(to_date)s
|
||||
or to_date between %(from_date)s and %(to_date)s
|
||||
or (from_date < %(from_date)s and to_date > %(to_date)s))
|
||||
"""
|
||||
if leave_type:
|
||||
query += "and leave_type=%(leave_type)s"
|
||||
|
||||
leave_applications = frappe.db.sql(
|
||||
query,
|
||||
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
|
||||
as_dict=1,
|
||||
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||
query = (
|
||||
frappe.qb.from_(LeaveApplication)
|
||||
.select(
|
||||
LeaveApplication.employee,
|
||||
LeaveApplication.leave_type,
|
||||
LeaveApplication.from_date,
|
||||
LeaveApplication.to_date,
|
||||
LeaveApplication.total_leave_days,
|
||||
)
|
||||
.where(
|
||||
(LeaveApplication.employee == employee)
|
||||
& (LeaveApplication.docstatus == 1)
|
||||
& (LeaveApplication.status == "Approved")
|
||||
& (
|
||||
(LeaveApplication.from_date.between(from_date, to_date))
|
||||
| (LeaveApplication.to_date.between(from_date, to_date))
|
||||
| ((LeaveApplication.from_date < from_date) & (LeaveApplication.to_date > to_date))
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
if leave_type:
|
||||
query = query.where(LeaveApplication.leave_type == leave_type)
|
||||
|
||||
leave_applications = query.run(as_dict=True)
|
||||
|
||||
leave_days = 0
|
||||
for leave_app in leave_applications:
|
||||
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
|
||||
|
@ -1,13 +1,14 @@
|
||||
frappe.listview_settings['Leave Application'] = {
|
||||
frappe.listview_settings["Leave Application"] = {
|
||||
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status === "Approved") {
|
||||
return [__("Approved"), "green", "status,=,Approved"];
|
||||
} else if (doc.status === "Rejected") {
|
||||
return [__("Rejected"), "red", "status,=,Rejected"];
|
||||
} else {
|
||||
return [__("Open"), "red", "status,=,Open"];
|
||||
}
|
||||
let status_color = {
|
||||
"Approved": "green",
|
||||
"Rejected": "red",
|
||||
"Open": "orange",
|
||||
"Cancelled": "red",
|
||||
"Submitted": "blue"
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
|
||||
}
|
||||
};
|
||||
|
@ -76,7 +76,14 @@ _test_records = [
|
||||
|
||||
class TestLeaveApplication(unittest.TestCase):
|
||||
def setUp(self):
|
||||
for dt in ["Leave Application", "Leave Allocation", "Salary Slip", "Leave Ledger Entry"]:
|
||||
for dt in [
|
||||
"Leave Application",
|
||||
"Leave Allocation",
|
||||
"Salary Slip",
|
||||
"Leave Ledger Entry",
|
||||
"Leave Period",
|
||||
"Leave Policy Assignment",
|
||||
]:
|
||||
frappe.db.delete(dt)
|
||||
|
||||
frappe.set_user("Administrator")
|
||||
@ -702,59 +709,24 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(details.leave_balance, 30)
|
||||
|
||||
def test_earned_leaves_creation(self):
|
||||
|
||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
leave_period = get_leave_period()
|
||||
employee = get_employee()
|
||||
leave_type = "Test Earned Leave Type"
|
||||
frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
|
||||
frappe.get_doc(
|
||||
dict(
|
||||
leave_type_name=leave_type,
|
||||
doctype="Leave Type",
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6,
|
||||
)
|
||||
).insert()
|
||||
make_policy_assignment(employee, leave_type, leave_period)
|
||||
|
||||
leave_policy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||
[employee.name], frappe._dict(data)
|
||||
)
|
||||
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
i = 0
|
||||
while i < 14:
|
||||
for i in range(0, 14):
|
||||
allocate_earned_leaves()
|
||||
i += 1
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||
|
||||
# validate earned leaves creation without maximum leaves
|
||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
||||
i = 0
|
||||
while i < 6:
|
||||
|
||||
for i in range(0, 6):
|
||||
allocate_earned_leaves()
|
||||
i += 1
|
||||
|
||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||
|
||||
# test to not consider current leave in leave balance while submitting
|
||||
@ -970,6 +942,54 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
self.assertEqual(leave_allocation["leaves_pending_approval"], 1)
|
||||
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_earned_leave_details_for_dashboard(self):
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
|
||||
leave_period = get_leave_period()
|
||||
employee = get_employee()
|
||||
leave_type = "Test Earned Leave Type"
|
||||
leave_policy_assignments = make_policy_assignment(employee, leave_type, leave_period)
|
||||
allocation = frappe.db.get_value(
|
||||
"Leave Allocation",
|
||||
{"leave_policy_assignment": leave_policy_assignments[0]},
|
||||
"name",
|
||||
)
|
||||
allocation = frappe.get_doc("Leave Allocation", allocation)
|
||||
allocation.new_leaves_allocated = 2
|
||||
allocation.save()
|
||||
|
||||
for i in range(0, 6):
|
||||
allocate_earned_leaves()
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list)
|
||||
make_leave_application(
|
||||
employee.name, add_days(first_sunday, 1), add_days(first_sunday, 1), leave_type
|
||||
)
|
||||
|
||||
details = get_leave_details(employee.name, allocation.from_date)
|
||||
leave_allocation = details["leave_allocation"][leave_type]
|
||||
expected = {
|
||||
"total_leaves": 2.0,
|
||||
"expired_leaves": 0.0,
|
||||
"leaves_taken": 1.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 1.0,
|
||||
}
|
||||
self.assertEqual(leave_allocation, expected)
|
||||
|
||||
details = get_leave_details(employee.name, getdate())
|
||||
leave_allocation = details["leave_allocation"][leave_type]
|
||||
|
||||
expected = {
|
||||
"total_leaves": 5.0,
|
||||
"expired_leaves": 0.0,
|
||||
"leaves_taken": 1.0,
|
||||
"leaves_pending_approval": 0.0,
|
||||
"remaining_leaves": 4.0,
|
||||
}
|
||||
self.assertEqual(leave_allocation, expected)
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_leave_allocation_records(self):
|
||||
employee = get_employee()
|
||||
@ -1100,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None):
|
||||
)[0][0]
|
||||
|
||||
return first_sunday
|
||||
|
||||
|
||||
def make_policy_assignment(employee, leave_type, leave_period):
|
||||
frappe.delete_doc_if_exists("Leave Type", leave_type, force=1)
|
||||
frappe.get_doc(
|
||||
dict(
|
||||
leave_type_name=leave_type,
|
||||
doctype="Leave Type",
|
||||
is_earned_leave=1,
|
||||
earned_leave_frequency="Monthly",
|
||||
rounding=0.5,
|
||||
max_leaves_allowed=6,
|
||||
)
|
||||
).insert()
|
||||
|
||||
leave_policy = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Leave Policy",
|
||||
"title": "Test Leave Policy",
|
||||
"leave_policy_details": [{"leave_type": leave_type, "annual_allocation": 6}],
|
||||
}
|
||||
).insert()
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
"leave_policy": leave_policy.name,
|
||||
"leave_period": leave_period.name,
|
||||
}
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees(
|
||||
[employee.name], frappe._dict(data)
|
||||
)
|
||||
return leave_policy_assignments
|
||||
|
@ -7,7 +7,7 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import getdate, nowdate
|
||||
|
||||
from erpnext.hr.doctype.leave_allocation.leave_allocation import get_unused_leaves
|
||||
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period
|
||||
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import create_leave_ledger_entry
|
||||
from erpnext.hr.utils import set_employee_name, validate_active_employee
|
||||
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
|
||||
@ -107,7 +107,10 @@ class LeaveEncashment(Document):
|
||||
self.leave_balance = (
|
||||
allocation.total_leaves_allocated
|
||||
- allocation.carry_forwarded_leaves_count
|
||||
- get_unused_leaves(self.employee, self.leave_type, allocation.from_date, self.encashment_date)
|
||||
# adding this because the function returns a -ve number
|
||||
+ get_leaves_for_period(
|
||||
self.employee, self.leave_type, allocation.from_date, self.encashment_date
|
||||
)
|
||||
)
|
||||
|
||||
encashable_days = self.leave_balance - frappe.db.get_value(
|
||||
@ -126,14 +129,25 @@ class LeaveEncashment(Document):
|
||||
return True
|
||||
|
||||
def get_leave_allocation(self):
|
||||
leave_allocation = frappe.db.sql(
|
||||
"""select name, to_date, total_leaves_allocated, carry_forwarded_leaves_count from `tabLeave Allocation` where '{0}'
|
||||
between from_date and to_date and docstatus=1 and leave_type='{1}'
|
||||
and employee= '{2}'""".format(
|
||||
self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
|
||||
),
|
||||
as_dict=1,
|
||||
) # nosec
|
||||
date = self.encashment_date or getdate()
|
||||
|
||||
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
|
||||
leave_allocation = (
|
||||
frappe.qb.from_(LeaveAllocation)
|
||||
.select(
|
||||
LeaveAllocation.name,
|
||||
LeaveAllocation.from_date,
|
||||
LeaveAllocation.to_date,
|
||||
LeaveAllocation.total_leaves_allocated,
|
||||
LeaveAllocation.carry_forwarded_leaves_count,
|
||||
)
|
||||
.where(
|
||||
((LeaveAllocation.from_date <= date) & (date <= LeaveAllocation.to_date))
|
||||
& (LeaveAllocation.docstatus == 1)
|
||||
& (LeaveAllocation.leave_type == self.leave_type)
|
||||
& (LeaveAllocation.employee == self.employee)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
return leave_allocation[0] if leave_allocation else None
|
||||
|
||||
|
@ -4,26 +4,42 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_months, today
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, get_year_ending, get_year_start, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
|
||||
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
|
||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||
create_assignment_for_multiple_employees,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
test_dependencies = ["Leave Type"]
|
||||
test_records = frappe.get_test_records("Leave Type")
|
||||
|
||||
|
||||
class TestLeaveEncashment(unittest.TestCase):
|
||||
class TestLeaveEncashment(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
||||
frappe.db.sql("""delete from `tabAdditional Salary`""")
|
||||
frappe.db.delete("Leave Period")
|
||||
frappe.db.delete("Leave Policy Assignment")
|
||||
frappe.db.delete("Leave Allocation")
|
||||
frappe.db.delete("Leave Ledger Entry")
|
||||
frappe.db.delete("Additional Salary")
|
||||
frappe.db.delete("Leave Encashment")
|
||||
|
||||
if not frappe.db.exists("Leave Type", "_Test Leave Type Encashment"):
|
||||
frappe.get_doc(test_records[2]).insert()
|
||||
|
||||
date = getdate()
|
||||
year_start = getdate(get_year_start(date))
|
||||
year_end = getdate(get_year_ending(date))
|
||||
|
||||
make_holiday_list("_Test Leave Encashment", year_start, year_end)
|
||||
|
||||
# create the leave policy
|
||||
leave_policy = create_leave_policy(
|
||||
@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
leave_policy.submit()
|
||||
|
||||
# create employee, salary structure and assignment
|
||||
self.employee = make_employee("test_employee_encashment@example.com")
|
||||
self.employee = make_employee("test_employee_encashment@example.com", company="_Test Company")
|
||||
|
||||
self.leave_period = create_leave_period(add_months(today(), -3), add_months(today(), 3))
|
||||
self.leave_period = create_leave_period(year_start, year_end, "_Test Company")
|
||||
|
||||
data = {
|
||||
"assignment_based_on": "Leave Period",
|
||||
@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
other_details={"leave_encashment_amount_per_day": 50},
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
for dt in [
|
||||
"Leave Period",
|
||||
"Leave Allocation",
|
||||
"Leave Ledger Entry",
|
||||
"Additional Salary",
|
||||
"Leave Encashment",
|
||||
"Salary Structure",
|
||||
"Leave Policy",
|
||||
]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
||||
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||
def test_leave_balance_value_and_amount(self):
|
||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
||||
leave_encashment = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Leave Encashment",
|
||||
employee=self.employee,
|
||||
leave_type="_Test Leave Type Encashment",
|
||||
leave_period=self.leave_period.name,
|
||||
payroll_date=today(),
|
||||
encashment_date=self.leave_period.to_date,
|
||||
currency="INR",
|
||||
)
|
||||
).insert()
|
||||
@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||
self.assertTrue(add_sal)
|
||||
|
||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
||||
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||
def test_leave_balance_value_with_leaves_and_amount(self):
|
||||
date = self.leave_period.from_date
|
||||
leave_application = make_leave_application(
|
||||
self.employee, date, add_days(date, 3), "_Test Leave Type Encashment"
|
||||
)
|
||||
leave_application.reload()
|
||||
|
||||
leave_encashment = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Leave Encashment",
|
||||
employee=self.employee,
|
||||
leave_type="_Test Leave Type Encashment",
|
||||
leave_period=self.leave_period.name,
|
||||
payroll_date=today(),
|
||||
encashment_date=self.leave_period.to_date,
|
||||
currency="INR",
|
||||
)
|
||||
).insert()
|
||||
|
||||
self.assertEqual(leave_encashment.leave_balance, 10 - leave_application.total_leave_days)
|
||||
# encashable days threshold is 5, total leaves are 6, so encashable days = 6-5 = 1
|
||||
# with charge of 50 per day
|
||||
self.assertEqual(leave_encashment.encashable_days, leave_encashment.leave_balance - 5)
|
||||
self.assertEqual(leave_encashment.encashment_amount, 50)
|
||||
|
||||
leave_encashment.submit()
|
||||
|
||||
# assert links
|
||||
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||
self.assertTrue(add_sal)
|
||||
|
||||
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
||||
leave_encashment = frappe.get_doc(
|
||||
dict(
|
||||
doctype="Leave Encashment",
|
||||
employee=self.employee,
|
||||
leave_type="_Test Leave Type Encashment",
|
||||
leave_period=self.leave_period.name,
|
||||
encashment_date=self.leave_period.to_date,
|
||||
currency="INR",
|
||||
)
|
||||
).insert()
|
||||
|
@ -172,27 +172,24 @@ class StaffingPlan(Document):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_designation_counts(designation, company):
|
||||
def get_designation_counts(designation, company, job_opening=None):
|
||||
if not designation:
|
||||
return False
|
||||
|
||||
employee_counts = {}
|
||||
company_set = get_descendants_of("Company", company)
|
||||
company_set.append(company)
|
||||
|
||||
employee_counts["employee_count"] = frappe.db.get_value(
|
||||
"Employee",
|
||||
filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
|
||||
fieldname=["count(name)"],
|
||||
employee_count = frappe.db.count(
|
||||
"Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)}
|
||||
)
|
||||
|
||||
employee_counts["job_openings"] = frappe.db.get_value(
|
||||
"Job Opening",
|
||||
filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
|
||||
fieldname=["count(name)"],
|
||||
)
|
||||
filters = {"designation": designation, "status": "Open", "company": ("in", company_set)}
|
||||
if job_opening:
|
||||
filters["name"] = ("!=", job_opening)
|
||||
|
||||
return employee_counts
|
||||
job_openings = frappe.db.count("Job Opening", filters)
|
||||
|
||||
return {"employee_count": employee_count, "job_openings": job_openings}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -85,13 +85,16 @@ def _set_up():
|
||||
make_company()
|
||||
|
||||
|
||||
def make_company():
|
||||
if frappe.db.exists("Company", "_Test Company 10"):
|
||||
def make_company(name=None, abbr=None):
|
||||
if not name:
|
||||
name = "_Test Company 10"
|
||||
|
||||
if frappe.db.exists("Company", name):
|
||||
return
|
||||
|
||||
company = frappe.new_doc("Company")
|
||||
company.company_name = "_Test Company 10"
|
||||
company.abbr = "_TC10"
|
||||
company.company_name = name
|
||||
company.abbr = abbr or "_TC10"
|
||||
company.parent_company = "_Test Company 3"
|
||||
company.default_currency = "INR"
|
||||
company.country = "Pakistan"
|
||||
|
@ -439,20 +439,18 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
|
||||
return False
|
||||
|
||||
|
||||
def get_salary_assignment(employee, date):
|
||||
assignment = frappe.db.sql(
|
||||
"""
|
||||
select * from `tabSalary Structure Assignment`
|
||||
where employee=%(employee)s
|
||||
and docstatus = 1
|
||||
and %(on_date)s >= from_date order by from_date desc limit 1""",
|
||||
{
|
||||
"employee": employee,
|
||||
"on_date": date,
|
||||
},
|
||||
as_dict=1,
|
||||
def get_salary_assignments(employee, payroll_period):
|
||||
start_date, end_date = frappe.db.get_value(
|
||||
"Payroll Period", payroll_period, ["start_date", "end_date"]
|
||||
)
|
||||
return assignment[0] if assignment else None
|
||||
assignments = frappe.db.get_all(
|
||||
"Salary Structure Assignment",
|
||||
filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]},
|
||||
fields=["*"],
|
||||
order_by="from_date",
|
||||
)
|
||||
|
||||
return assignments
|
||||
|
||||
|
||||
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
|
||||
|
@ -93,6 +93,12 @@ frappe.ui.form.on('Loan', {
|
||||
frm.trigger("make_loan_refund");
|
||||
},__('Create'));
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Loan Closure Requested" && frm.doc.is_term_loan && !frm.doc.is_secured_loan) {
|
||||
frm.add_custom_button(__('Close Loan'), function() {
|
||||
frm.trigger("close_unsecured_term_loan");
|
||||
},__('Status'));
|
||||
}
|
||||
}
|
||||
frm.trigger("toggle_fields");
|
||||
},
|
||||
@ -174,6 +180,18 @@ frappe.ui.form.on('Loan', {
|
||||
})
|
||||
},
|
||||
|
||||
close_unsecured_term_loan: function(frm) {
|
||||
frappe.call({
|
||||
args: {
|
||||
"loan": frm.doc.name
|
||||
},
|
||||
method: "erpnext.loan_management.doctype.loan.loan.close_unsecured_term_loan",
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
request_loan_closure: function(frm) {
|
||||
frappe.confirm(__("Do you really want to close this loan"),
|
||||
function() {
|
||||
|
@ -60,14 +60,16 @@ class Loan(AccountsController):
|
||||
)
|
||||
|
||||
def validate_cost_center(self):
|
||||
if not self.cost_center and self.rate_of_interest != 0:
|
||||
if not self.cost_center and self.rate_of_interest != 0.0:
|
||||
self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||
|
||||
if not self.cost_center:
|
||||
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
|
||||
if not self.cost_center:
|
||||
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
|
||||
|
||||
def on_submit(self):
|
||||
self.link_loan_security_pledge()
|
||||
# Interest accrual for backdated term loans
|
||||
self.accrue_loan_interest()
|
||||
|
||||
def on_cancel(self):
|
||||
self.unlink_loan_security_pledge()
|
||||
@ -187,6 +189,16 @@ class Loan(AccountsController):
|
||||
|
||||
self.db_set("maximum_loan_amount", maximum_loan_value)
|
||||
|
||||
def accrue_loan_interest(self):
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_term_loans,
|
||||
)
|
||||
|
||||
if getdate(self.repayment_start_date) < getdate() and self.is_term_loan:
|
||||
process_loan_interest_accrual_for_term_loans(
|
||||
posting_date=getdate(), loan_type=self.loan_type, loan=self.name
|
||||
)
|
||||
|
||||
def unlink_loan_security_pledge(self):
|
||||
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
|
||||
pledge_list = [d.name for d in pledges]
|
||||
@ -330,6 +342,22 @@ def get_loan_application(loan_application):
|
||||
return loan.as_dict()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def close_unsecured_term_loan(loan):
|
||||
loan_details = frappe.db.get_value(
|
||||
"Loan", {"name": loan}, ["status", "is_term_loan", "is_secured_loan"], as_dict=1
|
||||
)
|
||||
|
||||
if (
|
||||
loan_details.status == "Loan Closure Requested"
|
||||
and loan_details.is_term_loan
|
||||
and not loan_details.is_secured_loan
|
||||
):
|
||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||
else:
|
||||
frappe.throw(_("Cannot close this loan until full repayment"))
|
||||
|
||||
|
||||
def close_loan(loan, total_amount_paid):
|
||||
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
|
||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||
|
@ -448,8 +448,6 @@ class LoanRepayment(AccountsController):
|
||||
"remarks": remarks,
|
||||
"cost_center": self.cost_center,
|
||||
"posting_date": getdate(self.posting_date),
|
||||
"party_type": self.applicant_type if self.repay_from_salary else "",
|
||||
"party": self.applicant if self.repay_from_salary else "",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
@ -93,6 +93,11 @@ frappe.ui.form.on("BOM", {
|
||||
});
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("New Version"), function() {
|
||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||
frappe.set_route("Form", "BOM", new_bom.name);
|
||||
});
|
||||
|
||||
if(frm.doc.docstatus==1) {
|
||||
frm.add_custom_button(__("Work Order"), function() {
|
||||
frm.trigger("make_work_order");
|
||||
|
@ -22,6 +22,10 @@ from erpnext.stock.get_item_details import get_conversion_factor, get_price_list
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
|
||||
class BOMRecursionError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BOMTree:
|
||||
"""Full tree representation of a BOM"""
|
||||
|
||||
@ -252,9 +256,8 @@ class BOM(WebsiteGenerator):
|
||||
for item in self.get("items"):
|
||||
self.validate_bom_currency(item)
|
||||
|
||||
item.bom_no = ""
|
||||
if not item.do_not_explode:
|
||||
item.bom_no = item.bom_no
|
||||
if item.do_not_explode:
|
||||
item.bom_no = ""
|
||||
|
||||
ret = self.get_bom_material_detail(
|
||||
{
|
||||
@ -530,35 +533,27 @@ class BOM(WebsiteGenerator):
|
||||
"""Check whether recursion occurs in any bom"""
|
||||
|
||||
def _throw_error(bom_name):
|
||||
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name))
|
||||
frappe.throw(
|
||||
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
|
||||
exc=BOMRecursionError,
|
||||
)
|
||||
|
||||
bom_list = self.traverse_tree()
|
||||
child_items = (
|
||||
frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["bom_no", "item_code"],
|
||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||
)
|
||||
or []
|
||||
child_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["bom_no", "item_code"],
|
||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||
)
|
||||
|
||||
child_bom = {d.bom_no for d in child_items}
|
||||
child_items_codes = {d.item_code for d in child_items}
|
||||
for item in child_items:
|
||||
if self.name == item.bom_no:
|
||||
_throw_error(self.name)
|
||||
if self.item == item.item_code and item.bom_no:
|
||||
# Same item but with different BOM should not be allowed.
|
||||
# Same item can appear recursively once as long as it doesn't have BOM.
|
||||
_throw_error(item.bom_no)
|
||||
|
||||
if self.name in child_bom:
|
||||
_throw_error(self.name)
|
||||
|
||||
if self.item in child_items_codes:
|
||||
_throw_error(self.item)
|
||||
|
||||
bom_nos = (
|
||||
frappe.get_all(
|
||||
"BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"}
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
if self.name in {d.parent for d in bom_nos}:
|
||||
if self.name in {d.bom_no for d in self.items}:
|
||||
_throw_error(self.name)
|
||||
|
||||
def traverse_tree(self, bom_list=None):
|
||||
|
@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
||||
from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom
|
||||
from erpnext.manufacturing.doctype.bom.bom import BOMRecursionError, item_query, make_variant_bom
|
||||
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||
update_cost_in_all_boms_in_test,
|
||||
)
|
||||
@ -326,43 +326,36 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
def test_bom_recursion_1st_level(self):
|
||||
"""BOM should not allow BOM item again in child"""
|
||||
item_code = "_Test BOM Recursion"
|
||||
make_item(item_code, {"is_stock_item": 1})
|
||||
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.item = item_code
|
||||
bom.append("items", frappe._dict(item_code=item_code))
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
bom.save()
|
||||
with self.assertRaises(BOMRecursionError):
|
||||
bom.items[0].bom_no = bom.name
|
||||
bom.save()
|
||||
|
||||
self.assertTrue("recursion" in str(err.exception).lower())
|
||||
frappe.delete_doc("BOM", bom.name, ignore_missing=True)
|
||||
|
||||
def test_bom_recursion_transitive(self):
|
||||
item1 = "_Test BOM Recursion"
|
||||
item2 = "_Test BOM Recursion 2"
|
||||
make_item(item1, {"is_stock_item": 1})
|
||||
make_item(item2, {"is_stock_item": 1})
|
||||
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom1 = frappe.new_doc("BOM")
|
||||
bom1.item = item1
|
||||
bom1.append("items", frappe._dict(item_code=item2))
|
||||
bom1.save()
|
||||
bom1.submit()
|
||||
|
||||
bom2 = frappe.new_doc("BOM")
|
||||
bom2.item = item2
|
||||
bom2.append("items", frappe._dict(item_code=item1))
|
||||
bom2.save()
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
bom2.items[0].bom_no = bom1.name
|
||||
bom1.items[0].bom_no = bom2.name
|
||||
|
||||
with self.assertRaises(BOMRecursionError):
|
||||
bom1.save()
|
||||
bom2.save()
|
||||
bom2.submit()
|
||||
|
||||
self.assertTrue("recursion" in str(err.exception).lower())
|
||||
|
||||
bom1.cancel()
|
||||
frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True)
|
||||
frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True)
|
||||
|
||||
def test_bom_with_process_loss_item(self):
|
||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||
|
@ -42,6 +42,10 @@ class JobCardCancelError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class JobCardOverTransferError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class JobCard(Document):
|
||||
def onload(self):
|
||||
excess_transfer = frappe.db.get_single_value(
|
||||
@ -522,23 +526,50 @@ class JobCard(Document):
|
||||
},
|
||||
)
|
||||
|
||||
def set_transferred_qty_in_job_card(self, ste_doc):
|
||||
def set_transferred_qty_in_job_card_item(self, ste_doc):
|
||||
from frappe.query_builder.functions import Sum
|
||||
|
||||
def _validate_over_transfer(row, transferred_qty):
|
||||
"Block over transfer of items if not allowed in settings."
|
||||
required_qty = frappe.db.get_value("Job Card Item", row.job_card_item, "required_qty")
|
||||
is_excess = flt(transferred_qty) > flt(required_qty)
|
||||
if is_excess:
|
||||
frappe.throw(
|
||||
_(
|
||||
"Row #{0}: Cannot transfer more than Required Qty {1} for Item {2} against Job Card {3}"
|
||||
).format(
|
||||
row.idx, frappe.bold(required_qty), frappe.bold(row.item_code), ste_doc.job_card
|
||||
),
|
||||
title=_("Excess Transfer"),
|
||||
exc=JobCardOverTransferError,
|
||||
)
|
||||
|
||||
for row in ste_doc.items:
|
||||
if not row.job_card_item:
|
||||
continue
|
||||
|
||||
qty = frappe.db.sql(
|
||||
""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
|
||||
WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
|
||||
se.purpose = 'Material Transfer for Manufacture'
|
||||
""",
|
||||
(row.job_card_item),
|
||||
)[0][0]
|
||||
sed = frappe.qb.DocType("Stock Entry Detail")
|
||||
se = frappe.qb.DocType("Stock Entry")
|
||||
transferred_qty = (
|
||||
frappe.qb.from_(sed)
|
||||
.join(se)
|
||||
.on(sed.parent == se.name)
|
||||
.select(Sum(sed.qty))
|
||||
.where(
|
||||
(sed.job_card_item == row.job_card_item)
|
||||
& (se.docstatus == 1)
|
||||
& (se.purpose == "Material Transfer for Manufacture")
|
||||
)
|
||||
).run()[0][0]
|
||||
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(qty))
|
||||
allow_excess = frappe.db.get_single_value("Manufacturing Settings", "job_card_excess_transfer")
|
||||
if not allow_excess:
|
||||
_validate_over_transfer(row, transferred_qty)
|
||||
|
||||
frappe.db.set_value("Job Card Item", row.job_card_item, "transferred_qty", flt(transferred_qty))
|
||||
|
||||
def set_transferred_qty(self, update_status=False):
|
||||
"Set total FG Qty for which RM was transferred."
|
||||
"Set total FG Qty in Job Card for which RM was transferred."
|
||||
if not self.items:
|
||||
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
|
||||
|
||||
@ -590,7 +621,7 @@ class JobCard(Document):
|
||||
self.set_status(update_status)
|
||||
|
||||
def set_status(self, update_status=False):
|
||||
if self.status == "On Hold":
|
||||
if self.status == "On Hold" and self.docstatus == 0:
|
||||
return
|
||||
|
||||
self.status = {0: "Open", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
@ -866,6 +897,7 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
|
||||
target.set("time_logs", [])
|
||||
target.set("employee", [])
|
||||
target.set("items", [])
|
||||
target.set("sub_operations", [])
|
||||
target.set_sub_operations()
|
||||
target.get_required_items()
|
||||
target.validate_time_logs()
|
||||
|
@ -1,15 +1,25 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import random_string
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationMismatchError, OverlapError
|
||||
from typing import Literal
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase, change_settings
|
||||
from frappe.utils import random_string
|
||||
from frappe.utils.data import add_to_date, now
|
||||
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
JobCardOverTransferError,
|
||||
OperationMismatchError,
|
||||
OverlapError,
|
||||
make_corrective_job_card,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.job_card.job_card import (
|
||||
make_stock_entry as make_stock_entry_from_jc,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||
from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder
|
||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
|
||||
@ -17,34 +27,36 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
class TestJobCard(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_bom_for_jc_tests()
|
||||
self.transfer_material_against: Literal["Work Order", "Job Card"] = "Work Order"
|
||||
self.source_warehouse = None
|
||||
self._work_order = None
|
||||
|
||||
transfer_material_against, source_warehouse = None, None
|
||||
@property
|
||||
def work_order(self) -> WorkOrder:
|
||||
"""Work Order lazily created for tests."""
|
||||
if not self._work_order:
|
||||
self._work_order = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=2,
|
||||
transfer_material_against=self.transfer_material_against,
|
||||
source_warehouse=self.source_warehouse,
|
||||
)
|
||||
return self._work_order
|
||||
|
||||
tests_that_skip_setup = ("test_job_card_material_transfer_correctness",)
|
||||
tests_that_transfer_against_jc = (
|
||||
"test_job_card_multiple_materials_transfer",
|
||||
"test_job_card_excess_material_transfer",
|
||||
"test_job_card_partial_material_transfer",
|
||||
)
|
||||
|
||||
if self._testMethodName in tests_that_skip_setup:
|
||||
return
|
||||
|
||||
if self._testMethodName in tests_that_transfer_against_jc:
|
||||
transfer_material_against = "Job Card"
|
||||
source_warehouse = "Stores - _TC"
|
||||
|
||||
self.work_order = make_wo_order_test_record(
|
||||
item="_Test FG Item 2",
|
||||
qty=2,
|
||||
transfer_material_against=transfer_material_against,
|
||||
source_warehouse=source_warehouse,
|
||||
)
|
||||
def generate_required_stock(self, work_order: WorkOrder) -> None:
|
||||
"""Create twice the stock for all required items in work order."""
|
||||
for item in work_order.required_items:
|
||||
make_stock_entry(
|
||||
item_code=item.item_code,
|
||||
target=item.source_warehouse or self.source_warehouse,
|
||||
qty=item.required_qty * 2,
|
||||
basic_rate=100,
|
||||
)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_job_card(self):
|
||||
def test_job_card_operations(self):
|
||||
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card", filters={"work_order": self.work_order.name}, fields=["operation_id", "name"]
|
||||
@ -58,9 +70,6 @@ class TestJobCard(FrappeTestCase):
|
||||
doc.operation_id = "Test Data"
|
||||
self.assertRaises(OperationMismatchError, doc.save)
|
||||
|
||||
for d in job_cards:
|
||||
frappe.delete_doc("Job Card", d.name)
|
||||
|
||||
def test_job_card_with_different_work_station(self):
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
@ -96,19 +105,11 @@ class TestJobCard(FrappeTestCase):
|
||||
)
|
||||
self.assertEqual(completed_qty, job_card.for_quantity)
|
||||
|
||||
doc.cancel()
|
||||
|
||||
for d in job_cards:
|
||||
frappe.delete_doc("Job Card", d.name)
|
||||
|
||||
def test_job_card_overlap(self):
|
||||
wo2 = make_wo_order_test_record(item="_Test FG Item 2", qty=2)
|
||||
|
||||
jc1_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
jc2_name = frappe.db.get_value("Job Card", {"work_order": wo2.name})
|
||||
|
||||
jc1 = frappe.get_doc("Job Card", jc1_name)
|
||||
jc2 = frappe.get_doc("Job Card", jc2_name)
|
||||
jc1 = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
jc2 = frappe.get_last_doc("Job Card", {"work_order": wo2.name})
|
||||
|
||||
employee = "_T-Employee-00001" # from test records
|
||||
|
||||
@ -137,10 +138,10 @@ class TestJobCard(FrappeTestCase):
|
||||
|
||||
def test_job_card_multiple_materials_transfer(self):
|
||||
"Test transferring RMs separately against Job Card with multiple RMs."
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=10, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=6, basic_rate=100
|
||||
)
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
@ -165,16 +166,58 @@ class TestJobCard(FrappeTestCase):
|
||||
# transfer was made for 2 fg qty in first transfer Stock Entry
|
||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 0)
|
||||
|
||||
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 1})
|
||||
def test_job_card_excess_material_transfer(self):
|
||||
"Test transferring more than required RM against Job Card."
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
self.assertEqual(job_card.status, "Open")
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card.name)
|
||||
transfer_entry_1.insert()
|
||||
transfer_entry_1.submit()
|
||||
|
||||
# transfer extra qty of both RM due to previously damaged RM
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
|
||||
# deliberately change 'For Quantity'
|
||||
transfer_entry_2.fg_completed_qty = 1
|
||||
transfer_entry_2.items[0].qty = 5
|
||||
transfer_entry_2.items[1].qty = 3
|
||||
transfer_entry_2.insert()
|
||||
transfer_entry_2.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
||||
|
||||
# Check if 'For Quantity' is negative
|
||||
# as 'transferred_qty' > Qty to Manufacture
|
||||
transfer_entry_3 = make_stock_entry_from_jc(job_card.name)
|
||||
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
||||
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
|
||||
)
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
# JC is Completed with excess transfer
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
|
||||
@change_settings("Manufacturing Settings", {"job_card_excess_transfer": 0})
|
||||
def test_job_card_excess_material_transfer_block(self):
|
||||
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
self.assertEqual(job_card.status, "Open")
|
||||
|
||||
# fully transfer both RMs
|
||||
transfer_entry_1 = make_stock_entry_from_jc(job_card_name)
|
||||
@ -188,39 +231,19 @@ class TestJobCard(FrappeTestCase):
|
||||
transfer_entry_2.items[0].qty = 5
|
||||
transfer_entry_2.items[1].qty = 3
|
||||
transfer_entry_2.insert()
|
||||
transfer_entry_2.submit()
|
||||
|
||||
job_card.reload()
|
||||
self.assertGreater(job_card.transferred_qty, job_card.for_quantity)
|
||||
|
||||
# Check if 'For Quantity' is negative
|
||||
# as 'transferred_qty' > Qty to Manufacture
|
||||
transfer_entry_3 = make_stock_entry_from_jc(job_card_name)
|
||||
self.assertEqual(transfer_entry_3.fg_completed_qty, 0)
|
||||
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": "2021-01-01 00:01:00", "to_time": "2021-01-01 06:00:00", "completed_qty": 2},
|
||||
)
|
||||
job_card.save()
|
||||
job_card.submit()
|
||||
|
||||
# JC is Completed with excess transfer
|
||||
self.assertEqual(job_card.status, "Completed")
|
||||
self.assertRaises(JobCardOverTransferError, transfer_entry_2.submit)
|
||||
|
||||
def test_job_card_partial_material_transfer(self):
|
||||
"Test partial material transfer against Job Card"
|
||||
self.transfer_material_against = "Job Card"
|
||||
self.source_warehouse = "Stores - _TC"
|
||||
|
||||
make_stock_entry(item_code="_Test Item", target="Stores - _TC", qty=25, basic_rate=100)
|
||||
make_stock_entry(
|
||||
item_code="_Test Item Home Desktop Manufactured", target="Stores - _TC", qty=15, basic_rate=100
|
||||
)
|
||||
self.generate_required_stock(self.work_order)
|
||||
|
||||
job_card_name = frappe.db.get_value("Job Card", {"work_order": self.work_order.name})
|
||||
job_card = frappe.get_doc("Job Card", job_card_name)
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
|
||||
# partially transfer
|
||||
transfer_entry = make_stock_entry_from_jc(job_card_name)
|
||||
transfer_entry = make_stock_entry_from_jc(job_card.name)
|
||||
transfer_entry.fg_completed_qty = 1
|
||||
transfer_entry.get_items()
|
||||
transfer_entry.insert()
|
||||
@ -232,7 +255,7 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(transfer_entry.items[1].qty, 3)
|
||||
|
||||
# transfer remaining
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card_name)
|
||||
transfer_entry_2 = make_stock_entry_from_jc(job_card.name)
|
||||
|
||||
self.assertEqual(transfer_entry_2.fg_completed_qty, 1)
|
||||
self.assertEqual(transfer_entry_2.items[0].qty, 5)
|
||||
@ -277,7 +300,49 @@ class TestJobCard(FrappeTestCase):
|
||||
self.assertEqual(transfer_entry.items[0].item_code, "_Test Item")
|
||||
self.assertEqual(transfer_entry.items[0].qty, 2)
|
||||
|
||||
# rollback via tearDown method
|
||||
@change_settings(
|
||||
"Manufacturing Settings", {"add_corrective_operation_cost_in_finished_good_valuation": 1}
|
||||
)
|
||||
def test_corrective_costing(self):
|
||||
job_card = frappe.get_last_doc("Job Card", {"work_order": self.work_order.name})
|
||||
|
||||
job_card.append(
|
||||
"time_logs",
|
||||
{"from_time": now(), "to_time": add_to_date(now(), hours=1), "completed_qty": 2},
|
||||
)
|
||||
job_card.submit()
|
||||
|
||||
self.work_order.reload()
|
||||
original_cost = self.work_order.total_operating_cost
|
||||
|
||||
# Create a corrective operation against it
|
||||
corrective_action = frappe.get_doc(
|
||||
doctype="Operation", is_corrective_operation=1, name=frappe.generate_hash()
|
||||
).insert()
|
||||
|
||||
corrective_job_card = make_corrective_job_card(
|
||||
job_card.name, operation=corrective_action.name, for_operation=job_card.operation
|
||||
)
|
||||
corrective_job_card.hour_rate = 100
|
||||
corrective_job_card.insert()
|
||||
corrective_job_card.append(
|
||||
"time_logs",
|
||||
{
|
||||
"from_time": add_to_date(now(), hours=2),
|
||||
"to_time": add_to_date(now(), hours=2, minutes=30),
|
||||
"completed_qty": 2,
|
||||
},
|
||||
)
|
||||
corrective_job_card.submit()
|
||||
|
||||
self.work_order.reload()
|
||||
cost_after_correction = self.work_order.total_operating_cost
|
||||
self.assertGreater(cost_after_correction, original_cost)
|
||||
|
||||
corrective_job_card.cancel()
|
||||
self.work_order.reload()
|
||||
cost_after_cancel = self.work_order.total_operating_cost
|
||||
self.assertEqual(cost_after_cancel, original_cost)
|
||||
|
||||
|
||||
def create_bom_with_multiple_operations():
|
||||
|
@ -34,8 +34,7 @@ def get_data(filters):
|
||||
if filters.get(field):
|
||||
query_filters[field] = ("in", filters.get(field))
|
||||
|
||||
query_filters["report_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["report_date"] = ("<=", filters.get("to_date"))
|
||||
query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]]
|
||||
|
||||
return frappe.get_all(
|
||||
"Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc"
|
||||
|
@ -402,14 +402,15 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-01-13 17:40:09.474747",
|
||||
"modified": "2022-05-31 22:08:19.408223",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing",
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
"restrict_to_domain": "Manufacturing",
|
||||
"quick_lists": [],
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 17.0,
|
||||
"shortcuts": [
|
||||
|
@ -231,7 +231,6 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
|
||||
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
|
||||
erpnext.patches.v13_0.update_member_email_address
|
||||
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
||||
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
|
||||
erpnext.patches.v13_0.add_po_to_global_search
|
||||
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
|
||||
@ -359,7 +358,7 @@ erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr
|
||||
erpnext.patches.v13_0.update_accounts_in_loan_docs
|
||||
erpnext.patches.v14_0.update_batch_valuation_flag
|
||||
erpnext.patches.v14_0.delete_non_profit_doctypes
|
||||
erpnext.patches.v14_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.update_employee_advance_status
|
||||
erpnext.patches.v13_0.add_cost_center_in_loans
|
||||
erpnext.patches.v13_0.set_return_against_in_pos_invoice_references
|
||||
erpnext.patches.v13_0.remove_unknown_links_to_prod_plan_items # 24-03-2022
|
||||
@ -372,3 +371,6 @@ erpnext.patches.v14_0.discount_accounting_separation
|
||||
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
execute:frappe.delete_doc("DocType", "Naming Series")
|
||||
erpnext.patches.v13_0.set_payroll_entry_status
|
||||
erpnext.patches.v13_0.job_card_status_on_hold
|
||||
|
@ -1,94 +0,0 @@
|
||||
# Copyright (c) 2019, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("hr", "doctype", "leave_policy_assignment")
|
||||
frappe.reload_doc("hr", "doctype", "employee_grade")
|
||||
employee_with_assignment = []
|
||||
leave_policy = []
|
||||
|
||||
if "leave_policy" in frappe.db.get_table_columns("Employee"):
|
||||
employees_with_leave_policy = frappe.db.sql(
|
||||
"SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for employee in employees_with_leave_policy:
|
||||
alloc = frappe.db.exists(
|
||||
"Leave Allocation",
|
||||
{"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1},
|
||||
)
|
||||
if not alloc:
|
||||
create_assignment(employee.name, employee.leave_policy)
|
||||
|
||||
employee_with_assignment.append(employee.name)
|
||||
leave_policy.append(employee.leave_policy)
|
||||
|
||||
if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
|
||||
employee_grade_with_leave_policy = frappe.db.sql(
|
||||
"SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
# for whole employee Grade
|
||||
for grade in employee_grade_with_leave_policy:
|
||||
employees = get_employee_with_grade(grade.name)
|
||||
for employee in employees:
|
||||
|
||||
if employee not in employee_with_assignment: # Will ensure no duplicate
|
||||
alloc = frappe.db.exists(
|
||||
"Leave Allocation",
|
||||
{"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1},
|
||||
)
|
||||
if not alloc:
|
||||
create_assignment(employee.name, grade.default_leave_policy)
|
||||
leave_policy.append(grade.default_leave_policy)
|
||||
|
||||
# for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
|
||||
leave_allocations = frappe.db.sql(
|
||||
"SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for allocation in leave_allocations:
|
||||
if allocation.leave_policy not in leave_policy:
|
||||
create_assignment(
|
||||
allocation.employee,
|
||||
allocation.leave_policy,
|
||||
leave_period=allocation.leave_period,
|
||||
allocation_exists=True,
|
||||
)
|
||||
|
||||
|
||||
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False):
|
||||
if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
|
||||
return
|
||||
|
||||
filters = {"employee": employee, "leave_policy": leave_policy}
|
||||
if leave_period:
|
||||
filters["leave_period"] = leave_period
|
||||
|
||||
if not frappe.db.exists("Leave Policy Assignment", filters):
|
||||
lpa = frappe.new_doc("Leave Policy Assignment")
|
||||
lpa.employee = employee
|
||||
lpa.leave_policy = leave_policy
|
||||
|
||||
lpa.flags.ignore_mandatory = True
|
||||
if allocation_exists:
|
||||
lpa.assignment_based_on = "Leave Period"
|
||||
lpa.leave_period = leave_period
|
||||
lpa.leaves_allocated = 1
|
||||
|
||||
lpa.save()
|
||||
if allocation_exists:
|
||||
lpa.submit()
|
||||
# Updating old Leave Allocation
|
||||
frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
|
||||
|
||||
|
||||
def get_employee_with_grade(grade):
|
||||
return frappe.get_list("Employee", filters={"grade": grade})
|
@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
from erpnext.utilities.naming import set_by_naming_series
|
||||
|
||||
|
||||
def execute():
|
||||
|
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
19
erpnext/patches/v13_0/job_card_status_on_hold.py
Normal file
@ -0,0 +1,19 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
job_cards = frappe.get_all(
|
||||
"Job Card",
|
||||
{"status": "On Hold", "docstatus": ("!=", 0)},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
for idx, job_card in enumerate(job_cards):
|
||||
try:
|
||||
doc = frappe.get_doc("Job Card", job_card)
|
||||
doc.set_status()
|
||||
doc.db_set("status", doc.status, update_modified=False)
|
||||
if idx % 100 == 0:
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
continue
|
16
erpnext/patches/v13_0/set_payroll_entry_status.py
Normal file
16
erpnext/patches/v13_0/set_payroll_entry_status.py
Normal file
@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
from frappe.query_builder import Case
|
||||
|
||||
|
||||
def execute():
|
||||
PayrollEntry = frappe.qb.DocType("Payroll Entry")
|
||||
|
||||
(
|
||||
frappe.qb.update(PayrollEntry).set(
|
||||
"status",
|
||||
Case()
|
||||
.when(PayrollEntry.docstatus == 0, "Draft")
|
||||
.when(PayrollEntry.docstatus == 1, "Submitted")
|
||||
.else_("Cancelled"),
|
||||
)
|
||||
).run()
|
38
erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
Normal file
38
erpnext/patches/v14_0/migrate_gl_to_payment_ledger.py
Normal file
@ -0,0 +1,38 @@
|
||||
import frappe
|
||||
from frappe import qb
|
||||
|
||||
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
||||
get_dimensions,
|
||||
make_dimension_in_accounting_doctypes,
|
||||
)
|
||||
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||
|
||||
|
||||
def create_accounting_dimension_fields():
|
||||
dimensions_and_defaults = get_dimensions()
|
||||
if dimensions_and_defaults:
|
||||
for dimension in dimensions_and_defaults[0]:
|
||||
make_dimension_in_accounting_doctypes(dimension, ["Payment Ledger Entry"])
|
||||
|
||||
|
||||
def execute():
|
||||
# create accounting dimension fields in Payment Ledger
|
||||
create_accounting_dimension_fields()
|
||||
|
||||
gl = qb.DocType("GL Entry")
|
||||
accounts = frappe.db.get_list(
|
||||
"Account", "name", filters={"account_type": ["in", ["Receivable", "Payable"]]}, as_list=True
|
||||
)
|
||||
gl_entries = []
|
||||
if accounts:
|
||||
# get all gl entries on receivable/payable accounts
|
||||
gl_entries = (
|
||||
qb.from_(gl)
|
||||
.select("*")
|
||||
.where(gl.account.isin(accounts))
|
||||
.where(gl.is_cancelled == 0)
|
||||
.run(as_dict=True)
|
||||
)
|
||||
if gl_entries:
|
||||
# create payment ledger entries for the accounts receivable/payable
|
||||
create_payment_ledger_entry(gl_entries, 0)
|
@ -5,7 +5,7 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded
|
||||
from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded
|
||||
|
||||
from erpnext.hr.utils import (
|
||||
get_holiday_dates_for_employee,
|
||||
@ -27,11 +27,14 @@ class EmployeeBenefitApplication(Document):
|
||||
validate_active_employee(self.employee)
|
||||
self.validate_duplicate_on_payroll_period()
|
||||
if not self.max_benefits:
|
||||
self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period)
|
||||
self.max_benefits = flt(
|
||||
get_max_benefits_remaining(self.employee, self.date, self.payroll_period),
|
||||
self.precision("max_benefits"),
|
||||
)
|
||||
if self.max_benefits and self.max_benefits > 0:
|
||||
self.validate_max_benefit_for_component()
|
||||
self.validate_prev_benefit_claim()
|
||||
if self.remaining_benefit > 0:
|
||||
if self.remaining_benefit and self.remaining_benefit > 0:
|
||||
self.validate_remaining_benefit_amount()
|
||||
else:
|
||||
frappe.throw(
|
||||
@ -110,7 +113,7 @@ class EmployeeBenefitApplication(Document):
|
||||
max_benefit_amount = 0
|
||||
for employee_benefit in self.employee_benefits:
|
||||
self.validate_max_benefit(employee_benefit.earning_component)
|
||||
max_benefit_amount += employee_benefit.amount
|
||||
max_benefit_amount += flt(employee_benefit.amount)
|
||||
if max_benefit_amount > self.max_benefits:
|
||||
frappe.throw(
|
||||
_("Maximum benefit amount of employee {0} exceeds {1}").format(
|
||||
@ -125,7 +128,8 @@ class EmployeeBenefitApplication(Document):
|
||||
benefit_amount = 0
|
||||
for employee_benefit in self.employee_benefits:
|
||||
if employee_benefit.earning_component == earning_component_name:
|
||||
benefit_amount += employee_benefit.amount
|
||||
benefit_amount += flt(employee_benefit.amount)
|
||||
|
||||
prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(
|
||||
self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name
|
||||
)
|
||||
@ -207,26 +211,47 @@ def get_max_benefits_remaining(employee, on_date, payroll_period):
|
||||
def calculate_lwp(employee, start_date, holidays, working_days):
|
||||
lwp = 0
|
||||
holidays = "','".join(holidays)
|
||||
|
||||
for d in range(working_days):
|
||||
dt = add_days(cstr(getdate(start_date)), d)
|
||||
leave = frappe.db.sql(
|
||||
"""
|
||||
select t1.name, t1.half_day
|
||||
from `tabLeave Application` t1, `tabLeave Type` t2
|
||||
where t2.name = t1.leave_type
|
||||
and t2.is_lwp = 1
|
||||
and t1.docstatus = 1
|
||||
and t1.employee = %(employee)s
|
||||
and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
|
||||
WHEN t2.include_holiday THEN %(dt)s between from_date and to_date
|
||||
END
|
||||
""".format(
|
||||
holidays
|
||||
),
|
||||
{"employee": employee, "dt": dt},
|
||||
date = add_days(cstr(getdate(start_date)), d)
|
||||
|
||||
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||
LeaveType = frappe.qb.DocType("Leave Type")
|
||||
|
||||
is_half_day = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(
|
||||
(
|
||||
(LeaveApplication.half_day_date == date)
|
||||
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||
),
|
||||
LeaveApplication.half_day,
|
||||
)
|
||||
.else_(0)
|
||||
).as_("is_half_day")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(LeaveApplication)
|
||||
.inner_join(LeaveType)
|
||||
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||
.select(LeaveApplication.name, is_half_day)
|
||||
.where(
|
||||
(LeaveType.is_lwp == 1)
|
||||
& (LeaveApplication.docstatus == 1)
|
||||
& (LeaveApplication.status == "Approved")
|
||||
& (LeaveApplication.employee == employee)
|
||||
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||
)
|
||||
)
|
||||
if leave:
|
||||
lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1)
|
||||
|
||||
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||
if date in holidays:
|
||||
query = query.where((LeaveType.include_holiday == "1"))
|
||||
leaves = query.run(as_dict=True)
|
||||
|
||||
if leaves:
|
||||
lwp += 0.5 if leaves[0].is_half_day else 1
|
||||
|
||||
return lwp
|
||||
|
||||
|
||||
|
@ -3,6 +3,82 @@
|
||||
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate
|
||||
|
||||
class TestEmployeeBenefitApplication(unittest.TestCase):
|
||||
pass
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.hr.doctype.leave_application.test_leave_application import get_first_sunday
|
||||
from erpnext.hr.utils import get_holiday_dates_for_employee
|
||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||
calculate_lwp,
|
||||
)
|
||||
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||
create_payroll_period,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_holiday_list,
|
||||
make_leave_application,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
|
||||
class TestEmployeeBenefitApplication(FrappeTestCase):
|
||||
def setUp(self):
|
||||
date = getdate()
|
||||
make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date))
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_employee_benefit_application(self):
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
|
||||
employee = make_employee("test_employee_benefits@salary.com", company="_Test Company")
|
||||
first_sunday = get_first_sunday("Salary Slip Test Holiday List")
|
||||
|
||||
leave_application = make_leave_application(
|
||||
employee,
|
||||
add_days(first_sunday, 1),
|
||||
add_days(first_sunday, 3),
|
||||
"Leave Without Pay",
|
||||
half_day=1,
|
||||
half_day_date=add_days(first_sunday, 1),
|
||||
submit=True,
|
||||
)
|
||||
|
||||
frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0)
|
||||
salary_structure = make_salary_structure(
|
||||
"Test Employee Benefits",
|
||||
"Monthly",
|
||||
other_details={"max_benefits": 100000},
|
||||
include_flexi_benefits=True,
|
||||
employee=employee,
|
||||
payroll_period=payroll_period,
|
||||
)
|
||||
salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate())
|
||||
salary_slip.insert()
|
||||
salary_slip.submit()
|
||||
|
||||
application = make_employee_benefit_application(
|
||||
employee, payroll_period.name, date=leave_application.to_date
|
||||
)
|
||||
self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000)
|
||||
|
||||
holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date)
|
||||
working_days = date_diff(application.date, payroll_period.start_date) + 1
|
||||
lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days)
|
||||
self.assertEqual(lwp, 2.5)
|
||||
|
||||
|
||||
def make_employee_benefit_application(employee, payroll_period, date):
|
||||
frappe.db.delete("Employee Benefit Application")
|
||||
|
||||
return frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Benefit Application",
|
||||
"employee": employee,
|
||||
"date": date,
|
||||
"payroll_period": payroll_period,
|
||||
"employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}],
|
||||
}
|
||||
).insert()
|
||||
|
@ -33,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document):
|
||||
self.total_declared_amount += flt(d.amount)
|
||||
|
||||
def set_total_exemption_amount(self):
|
||||
self.total_exemption_amount = get_total_exemption_amount(self.declarations)
|
||||
self.total_exemption_amount = flt(
|
||||
get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount")
|
||||
)
|
||||
|
||||
def calculate_hra_exemption(self):
|
||||
self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0
|
||||
@ -41,9 +43,18 @@ class EmployeeTaxExemptionDeclaration(Document):
|
||||
hra_exemption = calculate_annual_eligible_hra_exemption(self)
|
||||
if hra_exemption:
|
||||
self.total_exemption_amount += hra_exemption["annual_exemption"]
|
||||
self.salary_structure_hra = hra_exemption["hra_amount"]
|
||||
self.annual_hra_exemption = hra_exemption["annual_exemption"]
|
||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
||||
self.total_exemption_amount = flt(
|
||||
self.total_exemption_amount, self.precision("total_exemption_amount")
|
||||
)
|
||||
self.salary_structure_hra = flt(
|
||||
hra_exemption["hra_amount"], self.precision("salary_structure_hra")
|
||||
)
|
||||
self.annual_hra_exemption = flt(
|
||||
hra_exemption["annual_exemption"], self.precision("annual_hra_exemption")
|
||||
)
|
||||
self.monthly_hra_exemption = flt(
|
||||
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -4,25 +4,28 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.utils import DuplicateDeclarationError
|
||||
|
||||
|
||||
class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
class TestEmployeeTaxExemptionDeclaration(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee@taxexepmtion.com")
|
||||
make_employee("employee1@taxexepmtion.com")
|
||||
create_payroll_period()
|
||||
make_employee("employee@taxexemption.com", company="_Test Company")
|
||||
make_employee("employee1@taxexemption.com", company="_Test Company")
|
||||
create_payroll_period(company="_Test Company")
|
||||
create_exemption_category()
|
||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""")
|
||||
frappe.db.delete("Employee Tax Exemption Declaration")
|
||||
frappe.db.delete("Salary Structure Assignment")
|
||||
|
||||
def test_duplicate_category_in_declaration(self):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -46,7 +49,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -68,7 +71,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
duplicate_declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -83,7 +86,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
)
|
||||
self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert)
|
||||
duplicate_declaration.employee = frappe.get_value(
|
||||
"Employee", {"user_id": "employee1@taxexepmtion.com"}, "name"
|
||||
"Employee", {"user_id": "employee1@taxexemption.com"}, "name"
|
||||
)
|
||||
self.assertTrue(duplicate_declaration.insert)
|
||||
|
||||
@ -91,7 +94,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -112,6 +115,298 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
|
||||
self.assertEqual(declaration.total_exemption_amount, 100000)
|
||||
|
||||
def test_india_hra_exemption(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Monthly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=80000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Monthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 3000)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 36000)
|
||||
# 100000 Standard Exemption + 36000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 136000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_daily_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Daily")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Daily HRA received = 3000
|
||||
# should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 17916.67)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 215000)
|
||||
# 50000 Standard Exemption + 215000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 265000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_weekly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Weekly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Weekly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 13000)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 156000)
|
||||
# 50000 Standard Exemption + 156000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 206000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_fortnightly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Fortnightly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Fortnightly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 6500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 78000)
|
||||
# 50000 Standard Exemption + 78000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 128000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_bimonthly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Bimonthly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=80000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Bimonthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 1500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 18000)
|
||||
# 100000 Standard Exemption + 18000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 118000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_multiple_salary_structure_assignments(self):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
make_salary_structure,
|
||||
)
|
||||
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
employee = make_employee("employee@taxexemption2.com", company="_Test Company")
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||
|
||||
create_tax_slab(
|
||||
payroll_period,
|
||||
allow_tax_exemption=True,
|
||||
currency="INR",
|
||||
effective_date=getdate("2019-04-01"),
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||
)
|
||||
|
||||
# salary structure with base 50000, HRA 3000
|
||||
make_salary_structure(
|
||||
"Monthly Structure for HRA Exemption 1",
|
||||
"Monthly",
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
from_date=payroll_period.start_date,
|
||||
)
|
||||
|
||||
# salary structure with base 70000, HRA = base * 0.2 = 14000
|
||||
salary_structure = make_salary_structure(
|
||||
"Monthly Structure for HRA Exemption 2",
|
||||
"Monthly",
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
from_date=payroll_period.start_date,
|
||||
dont_submit=True,
|
||||
)
|
||||
for component_row in salary_structure.earnings:
|
||||
if component_row.salary_component == "HRA":
|
||||
component_row.amount = 0
|
||||
component_row.amount_based_on_formula = 1
|
||||
component_row.formula = "base * 0.2"
|
||||
break
|
||||
|
||||
salary_structure.submit()
|
||||
|
||||
create_salary_structure_assignment(
|
||||
employee,
|
||||
salary_structure.name,
|
||||
from_date=add_months(payroll_period.start_date, 6),
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
base=70000,
|
||||
allow_duplicate=True,
|
||||
)
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": payroll_period.name,
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Monthly HRA received = 50000 * 6 months + 70000 * 6 months
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 8500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 102000)
|
||||
# 50000 Standard Exemption + 102000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 152000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
|
||||
def create_payroll_period(**args):
|
||||
args = frappe._dict(args)
|
||||
@ -163,3 +458,33 @@ def create_exemption_category():
|
||||
"is_active": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def setup_hra_exemption_prerequisites(frequency, employee=None):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||
if not employee:
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
create_tax_slab(
|
||||
payroll_period,
|
||||
allow_tax_exemption=True,
|
||||
currency="INR",
|
||||
effective_date=getdate("2019-04-01"),
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
make_salary_structure(
|
||||
f"{frequency} Structure for HRA Exemption",
|
||||
frequency,
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period,
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||
)
|
||||
|
@ -31,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
||||
self.total_actual_amount += flt(d.amount)
|
||||
|
||||
def set_total_exemption_amount(self):
|
||||
self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs)
|
||||
self.exemption_amount = flt(
|
||||
get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount")
|
||||
)
|
||||
|
||||
def calculate_hra_exemption(self):
|
||||
self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0
|
||||
@ -39,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
||||
hra_exemption = calculate_hra_exemption_for_period(self)
|
||||
if hra_exemption:
|
||||
self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
|
||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
||||
self.monthly_house_rent = hra_exemption["monthly_house_rent"]
|
||||
self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
|
||||
self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount"))
|
||||
self.monthly_hra_exemption = flt(
|
||||
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||
)
|
||||
self.monthly_house_rent = flt(
|
||||
hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent")
|
||||
)
|
||||
self.total_eligible_hra_exemption = flt(
|
||||
hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption")
|
||||
)
|
||||
|
@ -4,22 +4,26 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||
create_exemption_category,
|
||||
create_payroll_period,
|
||||
setup_hra_exemption_prerequisites,
|
||||
)
|
||||
|
||||
|
||||
class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
def setup(self):
|
||||
make_employee("employee@proofsubmission.com")
|
||||
create_payroll_period()
|
||||
class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee@proofsubmission.com", company="_Test Company")
|
||||
create_payroll_period(company="_Test Company")
|
||||
create_exemption_category()
|
||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""")
|
||||
frappe.db.delete("Employee Tax Exemption Proof Submission")
|
||||
frappe.db.delete("Salary Structure Assignment")
|
||||
|
||||
def test_exemption_amount_lesser_than_category_max(self):
|
||||
declaration = frappe.get_doc(
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||
@ -34,8 +38,8 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
||||
declaration = frappe.get_doc(
|
||||
self.assertRaises(frappe.ValidationError, proof.save)
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"payroll_period": "Test Payroll Period",
|
||||
@ -50,11 +54,11 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertTrue(declaration.save)
|
||||
self.assertTrue(declaration.submit)
|
||||
self.assertTrue(proof.save)
|
||||
self.assertTrue(proof.submit)
|
||||
|
||||
def test_duplicate_category_in_proof_submission(self):
|
||||
declaration = frappe.get_doc(
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||
@ -74,4 +78,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
||||
self.assertRaises(frappe.ValidationError, proof.save)
|
||||
|
||||
def test_india_hra_exemption(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name")
|
||||
setup_hra_exemption_prerequisites("Monthly", employee)
|
||||
payroll_period = frappe.db.get_value(
|
||||
"Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True
|
||||
)
|
||||
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"house_rent_payment_amount": 600000,
|
||||
"rented_in_metro_city": 1,
|
||||
"rented_from_date": payroll_period.start_date,
|
||||
"rented_to_date": payroll_period.end_date,
|
||||
"tax_exemption_proofs": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
type_of_proof="Test Proof",
|
||||
amount=100000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
type_of_proof="Test Proof",
|
||||
amount=50000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.assertEqual(proof.monthly_house_rent, 50000)
|
||||
|
||||
# Monthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(proof.monthly_hra_exemption, 3000)
|
||||
self.assertEqual(proof.total_eligible_hra_exemption, 36000)
|
||||
|
||||
# total exemptions + house rent payment amount
|
||||
self.assertEqual(proof.total_actual_amount, 750000)
|
||||
|
||||
# 100000 Standard Exemption + 36000 HRA exemption
|
||||
self.assertEqual(proof.exemption_amount, 136000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
@ -76,9 +76,8 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nUnpaid\nPaid",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
@ -194,7 +193,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-02 14:00:45.536152",
|
||||
"modified": "2022-05-27 13:56:14.349183",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Gratuity",
|
||||
|
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
@ -0,0 +1,12 @@
|
||||
frappe.listview_settings["Gratuity"] = {
|
||||
get_indicator: function(doc) {
|
||||
let status_color = {
|
||||
"Draft": "red",
|
||||
"Submitted": "blue",
|
||||
"Cancelled": "red",
|
||||
"Paid": "green",
|
||||
"Unpaid": "orange",
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||
}
|
||||
};
|
@ -4,57 +4,69 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, get_datetime, getdate
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_deduction_salary_component,
|
||||
make_earning_salary_component,
|
||||
make_employee_salary_slip,
|
||||
make_holiday_list,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
|
||||
|
||||
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
|
||||
|
||||
|
||||
class TestGratuity(unittest.TestCase):
|
||||
class TestGratuity(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Gratuity")
|
||||
frappe.db.delete("Salary Slip")
|
||||
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
|
||||
|
||||
make_earning_salary_component(
|
||||
setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
|
||||
)
|
||||
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
||||
make_holiday_list()
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
||||
new_employee = make_employee("new_employee@salary.com", company="_Test Company")
|
||||
salary_slip = get_last_salary_slip(new_employee)
|
||||
assert salary_slip is None
|
||||
self.assertIsNone(salary_slip)
|
||||
|
||||
def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_gratuity_based_on_current_slab_via_additional_salary(self):
|
||||
"""
|
||||
Range | Fraction
|
||||
5-0 | 1
|
||||
"""
|
||||
doj = add_days(getdate(), -(6 * 365))
|
||||
relieving_date = getdate()
|
||||
|
||||
employee = make_employee(
|
||||
"test_employee_gratuity@salary.com",
|
||||
company="_Test Company",
|
||||
date_of_joining=doj,
|
||||
relieving_date=relieving_date,
|
||||
)
|
||||
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||
|
||||
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
|
||||
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
|
||||
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
employee_total_workings_days = (
|
||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
||||
).days
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||
self.assertEqual(gratuity.current_work_experience, experience)
|
||||
|
||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
||||
gratuity.reload()
|
||||
from math import floor
|
||||
|
||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
||||
|
||||
# amount Calculation
|
||||
# amount calculation
|
||||
component_amount = frappe.get_all(
|
||||
"Salary Detail",
|
||||
filters={
|
||||
@ -64,20 +76,44 @@ class TestGratuity(unittest.TestCase):
|
||||
"salary_component": "Basic Salary",
|
||||
},
|
||||
fields=["amount"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
""" 5 - 0 fraction is 1 """
|
||||
|
||||
gratuity_amount = component_amount[0].amount * experience
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
# additional salary creation (Pay via salary slip)
|
||||
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
|
||||
|
||||
def test_check_gratuity_amount_based_on_all_previous_slabs(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
# gratuity should be marked "Paid" on the next salary slip submission
|
||||
salary_slip = make_salary_slip("Test Gratuity", employee=employee)
|
||||
salary_slip.posting_date = getdate()
|
||||
salary_slip.insert()
|
||||
salary_slip.submit()
|
||||
|
||||
gratuity.reload()
|
||||
self.assertEqual(gratuity.status, "Paid")
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self):
|
||||
"""
|
||||
Range | Fraction
|
||||
0-1 | 0
|
||||
1-5 | 0.7
|
||||
5-0 | 1
|
||||
"""
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
doj = add_days(getdate(), -(6 * 365))
|
||||
relieving_date = getdate()
|
||||
|
||||
employee = make_employee(
|
||||
"test_employee_gratuity@salary.com",
|
||||
company="_Test Company",
|
||||
date_of_joining=doj,
|
||||
relieving_date=relieving_date,
|
||||
)
|
||||
|
||||
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
|
||||
set_mode_of_payment_account()
|
||||
|
||||
@ -86,22 +122,11 @@ class TestGratuity(unittest.TestCase):
|
||||
)
|
||||
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
employee_total_workings_days = (
|
||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
||||
).days
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||
self.assertEqual(gratuity.current_work_experience, experience)
|
||||
|
||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
||||
|
||||
gratuity.reload()
|
||||
|
||||
from math import floor
|
||||
|
||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
||||
|
||||
# amount Calculation
|
||||
# amount calculation
|
||||
component_amount = frappe.get_all(
|
||||
"Salary Detail",
|
||||
filters={
|
||||
@ -111,35 +136,22 @@ class TestGratuity(unittest.TestCase):
|
||||
"salary_component": "Basic Salary",
|
||||
},
|
||||
fields=["amount"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
""" range | Fraction
|
||||
0-1 | 0
|
||||
1-5 | 0.7
|
||||
5-0 | 1
|
||||
"""
|
||||
|
||||
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
self.assertEqual(gratuity.status, "Unpaid")
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
pe = get_payment_entry("Gratuity", gratuity.name)
|
||||
pe.reference_no = "123467"
|
||||
pe.reference_date = getdate()
|
||||
pe.submit()
|
||||
|
||||
pay_entry = get_payment_entry("Gratuity", gratuity.name)
|
||||
pay_entry.reference_no = "123467"
|
||||
pay_entry.reference_date = getdate()
|
||||
pay_entry.save()
|
||||
pay_entry.submit()
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(gratuity.status, "Paid")
|
||||
self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def get_gratuity_rule(name):
|
||||
rule = frappe.db.exists("Gratuity Rule", name)
|
||||
@ -149,7 +161,6 @@ def get_gratuity_rule(name):
|
||||
rule.applicable_earnings_component = []
|
||||
rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"})
|
||||
rule.save()
|
||||
rule.reload()
|
||||
|
||||
return rule
|
||||
|
||||
@ -204,23 +215,17 @@ def create_account():
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_employee_and_get_last_salary_slip():
|
||||
employee = make_employee("test_employee@salary.com", company="_Test Company")
|
||||
frappe.db.set_value("Employee", employee, "relieving_date", getdate())
|
||||
frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365)))
|
||||
def create_salary_slip(employee):
|
||||
if not frappe.db.exists("Salary Slip", {"employee": employee}):
|
||||
salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
|
||||
posting_date = get_first_day(add_months(getdate(), -1))
|
||||
salary_slip = make_employee_salary_slip(
|
||||
employee, "Monthly", "Test Gratuity", posting_date=posting_date
|
||||
)
|
||||
salary_slip.start_date = posting_date
|
||||
salary_slip.end_date = None
|
||||
salary_slip.submit()
|
||||
salary_slip = salary_slip.name
|
||||
else:
|
||||
salary_slip = get_last_salary_slip(employee)
|
||||
|
||||
if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
|
||||
make_holiday_list()
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List"
|
||||
)
|
||||
|
||||
return employee, salary_slip
|
||||
return salary_slip
|
||||
|
@ -40,30 +40,69 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
if (!frm.is_new()) {
|
||||
if (frm.doc.docstatus === 0 && !frm.is_new()) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.add_custom_button(__("Get Employees"),
|
||||
function () {
|
||||
frm.events.get_employee_details(frm);
|
||||
}
|
||||
).toggleClass("btn-primary", !(frm.doc.employees || []).length);
|
||||
}
|
||||
|
||||
if (
|
||||
(frm.doc.employees || []).length
|
||||
&& !frappe.model.has_workflow(frm.doctype)
|
||||
&& !cint(frm.doc.salary_slips_created)
|
||||
&& (frm.doc.docstatus != 2)
|
||||
) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.add_custom_button(__("Get Employees"),
|
||||
function () {
|
||||
frm.events.get_employee_details(frm);
|
||||
}
|
||||
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
|
||||
}
|
||||
if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.page.set_primary_action(__('Create Salary Slips'), () => {
|
||||
frm.save('Submit').then(() => {
|
||||
frm.page.set_primary_action(__("Create Salary Slips"), () => {
|
||||
frm.save("Submit").then(() => {
|
||||
frm.page.clear_primary_action();
|
||||
frm.refresh();
|
||||
frm.events.refresh(frm);
|
||||
});
|
||||
});
|
||||
} else if (frm.doc.docstatus == 1 && frm.doc.status == "Failed") {
|
||||
frm.add_custom_button(__("Create Salary Slip"), function () {
|
||||
frm.call("create_salary_slips", {}, () => {
|
||||
frm.reload_doc();
|
||||
});
|
||||
}).addClass("btn-primary");
|
||||
}
|
||||
}
|
||||
if (frm.doc.docstatus == 1) {
|
||||
|
||||
if (frm.doc.docstatus == 1 && frm.doc.status == "Submitted") {
|
||||
if (frm.custom_buttons) frm.clear_custom_buttons();
|
||||
frm.events.add_context_buttons(frm);
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Failed" && frm.doc.error_message) {
|
||||
const issue = `<a id="jump_to_error" style="text-decoration: underline;">issue</a>`;
|
||||
let process = (cint(frm.doc.salary_slips_created)) ? "submission" : "creation";
|
||||
|
||||
frm.dashboard.set_headline(
|
||||
__("Salary Slip {0} failed. You can resolve the {1} and retry {0}.", [process, issue])
|
||||
);
|
||||
|
||||
$("#jump_to_error").on("click", (e) => {
|
||||
e.preventDefault();
|
||||
frappe.utils.scroll_to(
|
||||
frm.get_field("error_message").$wrapper,
|
||||
true,
|
||||
30
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
frappe.realtime.on("completed_salary_slip_creation", function() {
|
||||
frm.reload_doc();
|
||||
});
|
||||
|
||||
frappe.realtime.on("completed_salary_slip_submission", function() {
|
||||
frm.reload_doc();
|
||||
});
|
||||
},
|
||||
|
||||
get_employee_details: function (frm) {
|
||||
@ -88,7 +127,7 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
doc: frm.doc,
|
||||
method: "create_salary_slips",
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
frm.reload_doc();
|
||||
frm.toolbar.refresh();
|
||||
}
|
||||
});
|
||||
@ -97,7 +136,7 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
add_context_buttons: function (frm) {
|
||||
if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
|
||||
frm.events.add_bank_entry_button(frm);
|
||||
} else if (frm.doc.salary_slips_created) {
|
||||
} else if (frm.doc.salary_slips_created && frm.doc.status != 'Queued') {
|
||||
frm.add_custom_button(__("Submit Salary Slip"), function () {
|
||||
submit_salary_slip(frm);
|
||||
}).addClass("btn-primary");
|
||||
@ -331,6 +370,7 @@ const submit_salary_slip = function (frm) {
|
||||
method: 'submit_salary_slips',
|
||||
args: {},
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
frm.events.refresh(frm);
|
||||
},
|
||||
doc: frm.doc,
|
||||
|
@ -8,11 +8,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break0",
|
||||
"column_break0",
|
||||
"posting_date",
|
||||
"payroll_frequency",
|
||||
"company",
|
||||
"column_break1",
|
||||
"status",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"payroll_payable_account",
|
||||
@ -41,11 +41,14 @@
|
||||
"cost_center",
|
||||
"account",
|
||||
"payment_account",
|
||||
"amended_from",
|
||||
"column_break_33",
|
||||
"bank_account",
|
||||
"salary_slips_created",
|
||||
"salary_slips_submitted"
|
||||
"salary_slips_submitted",
|
||||
"failure_details_section",
|
||||
"error_message",
|
||||
"section_break_41",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -53,11 +56,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Select Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break0",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
@ -231,6 +229,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Salary Slips Created",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -239,6 +238,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Salary Slips Submitted",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -284,15 +284,44 @@
|
||||
"label": "Payroll Payable Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "error_message",
|
||||
"depends_on": "eval:doc.status=='Failed';",
|
||||
"fieldname": "failure_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Failure Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status=='Failed';",
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Error Message",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_41",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Draft\nSubmitted\nCancelled\nQueued\nFailed",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-17 15:13:17.766210",
|
||||
"modified": "2022-03-16 12:45:21.662765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Payroll Entry",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -308,5 +337,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@ -16,6 +17,7 @@ from frappe.utils import (
|
||||
comma_and,
|
||||
date_diff,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
)
|
||||
|
||||
@ -39,16 +41,28 @@ class PayrollEntry(Document):
|
||||
|
||||
def validate(self):
|
||||
self.number_of_employees = len(self.employees)
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
self.set_status(update=True, status="Submitted")
|
||||
self.create_salary_slips()
|
||||
|
||||
def before_submit(self):
|
||||
self.validate_employee_details()
|
||||
self.validate_payroll_payable_account()
|
||||
if self.validate_attendance:
|
||||
if self.validate_employee_attendance():
|
||||
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
||||
|
||||
def set_status(self, status=None, update=False):
|
||||
if not status:
|
||||
status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
if update:
|
||||
self.db_set("status", status)
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
def validate_employee_details(self):
|
||||
emp_with_sal_slip = []
|
||||
for employee_details in self.employees:
|
||||
@ -66,6 +80,14 @@ class PayrollEntry(Document):
|
||||
if len(emp_with_sal_slip):
|
||||
frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip)))
|
||||
|
||||
def validate_payroll_payable_account(self):
|
||||
if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"):
|
||||
frappe.throw(
|
||||
_(
|
||||
"Account type cannot be set for payroll payable account {0}, please remove and try again"
|
||||
).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account)))
|
||||
)
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.delete_doc(
|
||||
"Salary Slip",
|
||||
@ -77,6 +99,8 @@ class PayrollEntry(Document):
|
||||
)
|
||||
self.db_set("salary_slips_created", 0)
|
||||
self.db_set("salary_slips_submitted", 0)
|
||||
self.set_status(update=True, status="Cancelled")
|
||||
self.db_set("error_message", "")
|
||||
|
||||
def get_emp_list(self):
|
||||
"""
|
||||
@ -173,8 +197,20 @@ class PayrollEntry(Document):
|
||||
"currency": self.currency,
|
||||
}
|
||||
)
|
||||
if len(employees) > 30:
|
||||
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
|
||||
if len(employees) > 30 or frappe.flags.enqueue_payroll_entry:
|
||||
self.db_set("status", "Queued")
|
||||
frappe.enqueue(
|
||||
create_salary_slips_for_employees,
|
||||
timeout=600,
|
||||
employees=employees,
|
||||
args=args,
|
||||
publish_progress=False,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Salary Slip creation is queued. It may take a few minutes"),
|
||||
alert=True,
|
||||
indicator="blue",
|
||||
)
|
||||
else:
|
||||
create_salary_slips_for_employees(employees, args, publish_progress=False)
|
||||
# since this method is called via frm.call this doc needs to be updated manually
|
||||
@ -204,13 +240,23 @@ class PayrollEntry(Document):
|
||||
@frappe.whitelist()
|
||||
def submit_salary_slips(self):
|
||||
self.check_permission("write")
|
||||
ss_list = self.get_sal_slip_list(ss_status=0)
|
||||
if len(ss_list) > 30:
|
||||
salary_slips = self.get_sal_slip_list(ss_status=0)
|
||||
if len(salary_slips) > 30 or frappe.flags.enqueue_payroll_entry:
|
||||
self.db_set("status", "Queued")
|
||||
frappe.enqueue(
|
||||
submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list
|
||||
submit_salary_slips_for_employees,
|
||||
timeout=600,
|
||||
payroll_entry=self,
|
||||
salary_slips=salary_slips,
|
||||
publish_progress=False,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Salary Slip submission is queued. It may take a few minutes"),
|
||||
alert=True,
|
||||
indicator="blue",
|
||||
)
|
||||
else:
|
||||
submit_salary_slips_for_employees(self, ss_list, publish_progress=False)
|
||||
submit_salary_slips_for_employees(self, salary_slips, publish_progress=False)
|
||||
|
||||
def email_salary_slip(self, submitted_ss):
|
||||
if frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"):
|
||||
@ -223,7 +269,11 @@ class PayrollEntry(Document):
|
||||
)
|
||||
|
||||
if not account:
|
||||
frappe.throw(_("Please set account in Salary Component {0}").format(salary_component))
|
||||
frappe.throw(
|
||||
_("Please set account in Salary Component {0}").format(
|
||||
get_link_to_form("Salary Component", salary_component)
|
||||
)
|
||||
)
|
||||
|
||||
return account
|
||||
|
||||
@ -780,36 +830,80 @@ def payroll_entry_has_bank_entries(name):
|
||||
return response
|
||||
|
||||
|
||||
def log_payroll_failure(process, payroll_entry, error):
|
||||
error_log = frappe.log_error(
|
||||
title=_("Salary Slip {0} failed for Payroll Entry {1}").format(process, payroll_entry.name)
|
||||
)
|
||||
message_log = frappe.message_log.pop() if frappe.message_log else str(error)
|
||||
|
||||
try:
|
||||
error_message = json.loads(message_log).get("message")
|
||||
except Exception:
|
||||
error_message = message_log
|
||||
|
||||
error_message += "\n" + _("Check Error Log {0} for more details.").format(
|
||||
get_link_to_form("Error Log", error_log.name)
|
||||
)
|
||||
|
||||
payroll_entry.db_set({"error_message": error_message, "status": "Failed"})
|
||||
|
||||
|
||||
def create_salary_slips_for_employees(employees, args, publish_progress=True):
|
||||
salary_slips_exists_for = get_existing_salary_slips(employees, args)
|
||||
count = 0
|
||||
salary_slips_not_created = []
|
||||
for emp in employees:
|
||||
if emp not in salary_slips_exists_for:
|
||||
args.update({"doctype": "Salary Slip", "employee": emp})
|
||||
ss = frappe.get_doc(args)
|
||||
ss.insert()
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(
|
||||
count * 100 / len(set(employees) - set(salary_slips_exists_for)),
|
||||
title=_("Creating Salary Slips..."),
|
||||
)
|
||||
try:
|
||||
payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
|
||||
salary_slips_exist_for = get_existing_salary_slips(employees, args)
|
||||
count = 0
|
||||
|
||||
else:
|
||||
salary_slips_not_created.append(emp)
|
||||
for emp in employees:
|
||||
if emp not in salary_slips_exist_for:
|
||||
args.update({"doctype": "Salary Slip", "employee": emp})
|
||||
frappe.get_doc(args).insert()
|
||||
|
||||
payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
|
||||
payroll_entry.db_set("salary_slips_created", 1)
|
||||
payroll_entry.notify_update()
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(
|
||||
count * 100 / len(set(employees) - set(salary_slips_exist_for)),
|
||||
title=_("Creating Salary Slips..."),
|
||||
)
|
||||
|
||||
if salary_slips_not_created:
|
||||
payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1, "error_message": ""})
|
||||
|
||||
if salary_slips_exist_for:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Salary Slips already exist for employees {}, and will not be processed by this payroll."
|
||||
).format(frappe.bold(", ".join(emp for emp in salary_slips_exist_for))),
|
||||
title=_("Message"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
log_payroll_failure("creation", payroll_entry, e)
|
||||
|
||||
finally:
|
||||
frappe.db.commit() # nosemgrep
|
||||
frappe.publish_realtime("completed_salary_slip_creation")
|
||||
|
||||
|
||||
def show_payroll_submission_status(submitted, unsubmitted, payroll_entry):
|
||||
if not submitted and not unsubmitted:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Salary Slips already exists for employees {}, and will not be processed by this payroll."
|
||||
).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))),
|
||||
title=_("Message"),
|
||||
indicator="orange",
|
||||
"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
|
||||
)
|
||||
)
|
||||
elif submitted and not unsubmitted:
|
||||
frappe.msgprint(
|
||||
_("Salary Slips submitted for period from {0} to {1}").format(
|
||||
payroll_entry.start_date, payroll_entry.end_date
|
||||
)
|
||||
)
|
||||
elif unsubmitted:
|
||||
frappe.msgprint(
|
||||
_("Could not submit some Salary Slips: {}").format(
|
||||
", ".join(get_link_to_form("Salary Slip", entry) for entry in unsubmitted)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -827,45 +921,41 @@ def get_existing_salary_slips(employees, args):
|
||||
|
||||
|
||||
def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
|
||||
submitted_ss = []
|
||||
not_submitted_ss = []
|
||||
frappe.flags.via_payroll_entry = True
|
||||
try:
|
||||
submitted = []
|
||||
unsubmitted = []
|
||||
frappe.flags.via_payroll_entry = True
|
||||
count = 0
|
||||
|
||||
count = 0
|
||||
for ss in salary_slips:
|
||||
ss_obj = frappe.get_doc("Salary Slip", ss[0])
|
||||
if ss_obj.net_pay < 0:
|
||||
not_submitted_ss.append(ss[0])
|
||||
else:
|
||||
try:
|
||||
ss_obj.submit()
|
||||
submitted_ss.append(ss_obj)
|
||||
except frappe.ValidationError:
|
||||
not_submitted_ss.append(ss[0])
|
||||
for entry in salary_slips:
|
||||
salary_slip = frappe.get_doc("Salary Slip", entry[0])
|
||||
if salary_slip.net_pay < 0:
|
||||
unsubmitted.append(entry[0])
|
||||
else:
|
||||
try:
|
||||
salary_slip.submit()
|
||||
submitted.append(salary_slip)
|
||||
except frappe.ValidationError:
|
||||
unsubmitted.append(entry[0])
|
||||
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
|
||||
if submitted_ss:
|
||||
payroll_entry.make_accrual_jv_entry()
|
||||
frappe.msgprint(
|
||||
_("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date)
|
||||
)
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
|
||||
|
||||
payroll_entry.email_salary_slip(submitted_ss)
|
||||
if submitted:
|
||||
payroll_entry.make_accrual_jv_entry()
|
||||
payroll_entry.email_salary_slip(submitted)
|
||||
payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""})
|
||||
|
||||
payroll_entry.db_set("salary_slips_submitted", 1)
|
||||
payroll_entry.notify_update()
|
||||
show_payroll_submission_status(submitted, unsubmitted, payroll_entry)
|
||||
|
||||
if not submitted_ss and not not_submitted_ss:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
log_payroll_failure("submission", payroll_entry, e)
|
||||
|
||||
if not_submitted_ss:
|
||||
frappe.msgprint(_("Could not submit some Salary Slips"))
|
||||
finally:
|
||||
frappe.db.commit() # nosemgrep
|
||||
frappe.publish_realtime("completed_salary_slip_submission")
|
||||
|
||||
frappe.flags.via_payroll_entry = False
|
||||
|
||||
|
18
erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
Normal file
18
erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings['Payroll Entry'] = {
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function(doc) {
|
||||
var status_color = {
|
||||
'Draft': 'red',
|
||||
'Submitted': 'blue',
|
||||
'Queued': 'orange',
|
||||
'Failed': 'red',
|
||||
'Cancelled': 'red'
|
||||
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
|
||||
}
|
||||
};
|
@ -5,6 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months
|
||||
|
||||
import erpnext
|
||||
@ -22,10 +23,9 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
|
||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_end_date, get_start_end_dates
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
create_account,
|
||||
get_salary_component_account,
|
||||
make_deduction_salary_component,
|
||||
make_earning_salary_component,
|
||||
make_employee_salary_slip,
|
||||
set_salary_component_account,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
@ -35,13 +35,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
test_dependencies = ["Holiday List"]
|
||||
|
||||
|
||||
class TestPayrollEntry(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.set_value(
|
||||
"Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List"
|
||||
)
|
||||
|
||||
class TestPayrollEntry(FrappeTestCase):
|
||||
def setUp(self):
|
||||
for dt in [
|
||||
"Salary Slip",
|
||||
@ -52,81 +46,72 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
"Salary Structure Assignment",
|
||||
"Payroll Employee Detail",
|
||||
"Additional Salary",
|
||||
"Loan",
|
||||
]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
frappe.db.delete(dt)
|
||||
|
||||
make_earning_salary_component(setup=True, company_list=["_Test Company"])
|
||||
make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", "_Test Holiday List")
|
||||
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
|
||||
|
||||
def test_payroll_entry(self): # pylint: disable=no-self-use
|
||||
company = erpnext.get_default_company()
|
||||
for data in frappe.get_all("Salary Component", fields=["name"]):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data.name, "company": company}, "name"
|
||||
):
|
||||
get_salary_component_account(data.name)
|
||||
|
||||
employee = frappe.db.get_value("Employee", {"company": company})
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
make_salary_structure(
|
||||
"_Test Salary Structure",
|
||||
"Monthly",
|
||||
employee,
|
||||
company=company,
|
||||
currency=company_doc.default_currency,
|
||||
# set default payable account
|
||||
default_account = frappe.db.get_value(
|
||||
"Company", "_Test Company", "default_payroll_payable_account"
|
||||
)
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
if not frappe.db.get_value(
|
||||
"Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
|
||||
):
|
||||
make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
if not default_account or default_account != "_Test Payroll Payable - _TC":
|
||||
create_account(
|
||||
account_name="_Test Payroll Payable",
|
||||
company="_Test Company",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_type="Payable",
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
|
||||
)
|
||||
|
||||
def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use
|
||||
company = erpnext.get_default_company()
|
||||
employee = make_employee("test_muti_currency_employee@payroll.com", company=company)
|
||||
for data in frappe.get_all("Salary Component", fields=["name"]):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data.name, "company": company}, "name"
|
||||
):
|
||||
get_salary_component_account(data.name)
|
||||
def test_payroll_entry(self):
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
employee = frappe.db.get_value("Employee", {"company": "_Test Company"})
|
||||
setup_salary_structure(employee, company)
|
||||
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
salary_structure = make_salary_structure(
|
||||
"_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD"
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company.default_payroll_payable_account,
|
||||
currency=company.default_currency,
|
||||
company=company.name,
|
||||
)
|
||||
create_salary_structure_assignment(
|
||||
employee, salary_structure.name, company=company, currency="USD"
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSalary Slip` where employee=%s""",
|
||||
(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})),
|
||||
)
|
||||
salary_slip = get_salary_slip(
|
||||
"test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure"
|
||||
|
||||
def test_multi_currency_payroll_entry(self):
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
employee = make_employee(
|
||||
"test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC"
|
||||
)
|
||||
salary_structure = "_Test Multi Currency Salary Structure"
|
||||
setup_salary_structure(employee, company, "USD", salary_structure)
|
||||
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
payable_account=company.default_payroll_payable_account,
|
||||
currency="USD",
|
||||
exchange_rate=70,
|
||||
company=company.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
payroll_entry.make_payment_entry()
|
||||
|
||||
salary_slip.load_from_db()
|
||||
salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}, "name")
|
||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip)
|
||||
|
||||
payroll_entry.reload()
|
||||
payroll_je = salary_slip.journal_entry
|
||||
if payroll_je:
|
||||
payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je)
|
||||
|
||||
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
|
||||
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
|
||||
|
||||
@ -139,27 +124,15 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
(payroll_entry.name),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit)
|
||||
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit)
|
||||
|
||||
def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use
|
||||
for data in frappe.get_all("Salary Component", fields=["name"]):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name"
|
||||
):
|
||||
get_salary_component_account(data.name)
|
||||
|
||||
def test_payroll_entry_with_employee_cost_center(self):
|
||||
if not frappe.db.exists("Department", "cc - _TC"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Department", "department_name": "cc", "company": "_Test Company"}
|
||||
).insert()
|
||||
|
||||
frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """)
|
||||
frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """)
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """)
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """)
|
||||
|
||||
employee1 = make_employee(
|
||||
"test_employee1@example.com",
|
||||
payroll_cost_center="_Test Cost Center - _TC",
|
||||
@ -170,38 +143,15 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
"test_employee2@example.com", department="cc - _TC", company="_Test Company"
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
|
||||
create_account(
|
||||
account_name="_Test Payroll Payable",
|
||||
company="_Test Company",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_type="Payable",
|
||||
)
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
setup_salary_structure(employee1, company)
|
||||
|
||||
if (
|
||||
not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
|
||||
or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
|
||||
!= "_Test Payroll Payable - _TC"
|
||||
):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
|
||||
)
|
||||
currency = frappe.db.get_value("Company", "_Test Company", "default_currency")
|
||||
|
||||
make_salary_structure(
|
||||
"_Test Salary Structure 1",
|
||||
"Monthly",
|
||||
employee1,
|
||||
company="_Test Company",
|
||||
currency=currency,
|
||||
test_tax=False,
|
||||
)
|
||||
ss = make_salary_structure(
|
||||
"_Test Salary Structure 2",
|
||||
"Monthly",
|
||||
employee2,
|
||||
company="_Test Company",
|
||||
currency=currency,
|
||||
currency=company.default_currency,
|
||||
test_tax=False,
|
||||
)
|
||||
|
||||
@ -220,42 +170,38 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
ssa_doc.append(
|
||||
"payroll_cost_centers", {"cost_center": "_Test Cost Center 2 - _TC", "percentage": 40}
|
||||
)
|
||||
|
||||
ssa_doc.save()
|
||||
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
if not frappe.db.get_value(
|
||||
"Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
|
||||
):
|
||||
pe = make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account="_Test Payroll Payable - _TC",
|
||||
currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
|
||||
department="cc - _TC",
|
||||
company="_Test Company",
|
||||
payment_account="Cash - _TC",
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
|
||||
je_entries = frappe.db.sql(
|
||||
"""
|
||||
select account, cost_center, debit, credit
|
||||
from `tabJournal Entry Account`
|
||||
where parent=%s
|
||||
order by account, cost_center
|
||||
""",
|
||||
je,
|
||||
)
|
||||
expected_je = (
|
||||
("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
|
||||
("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
|
||||
("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
|
||||
)
|
||||
pe = make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account="_Test Payroll Payable - _TC",
|
||||
currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
|
||||
department="cc - _TC",
|
||||
company="_Test Company",
|
||||
payment_account="Cash - _TC",
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
|
||||
je_entries = frappe.db.sql(
|
||||
"""
|
||||
select account, cost_center, debit, credit
|
||||
from `tabJournal Entry Account`
|
||||
where parent=%s
|
||||
order by account, cost_center
|
||||
""",
|
||||
je,
|
||||
)
|
||||
expected_je = (
|
||||
("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
|
||||
("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
|
||||
("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
|
||||
)
|
||||
|
||||
self.assertEqual(je_entries, expected_je)
|
||||
self.assertEqual(je_entries, expected_je)
|
||||
|
||||
def test_get_end_date(self):
|
||||
self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"})
|
||||
@ -268,31 +214,22 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"})
|
||||
|
||||
def test_loan(self):
|
||||
branch = "Test Employee Branch"
|
||||
applicant = make_employee("test_employee@loan.com", company="_Test Company")
|
||||
company = "_Test Company"
|
||||
holiday_list = make_holiday("test holiday for loan")
|
||||
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
if not company_doc.default_payroll_payable_account:
|
||||
company_doc.default_payroll_payable_account = frappe.db.get_value(
|
||||
"Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name"
|
||||
)
|
||||
company_doc.save()
|
||||
branch = "Test Employee Branch"
|
||||
|
||||
if not frappe.db.exists("Branch", branch):
|
||||
frappe.get_doc({"doctype": "Branch", "branch": branch}).insert()
|
||||
holiday_list = make_holiday("test holiday for loan")
|
||||
|
||||
employee_doc = frappe.get_doc("Employee", applicant)
|
||||
employee_doc.branch = branch
|
||||
employee_doc.holiday_list = holiday_list
|
||||
employee_doc.save()
|
||||
applicant = make_employee(
|
||||
"test_employee@loan.com", company="_Test Company", branch=branch, holiday_list=holiday_list
|
||||
)
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
|
||||
salary_structure = "Test Salary Structure for Loan"
|
||||
make_salary_structure(
|
||||
salary_structure,
|
||||
"Test Salary Structure for Loan",
|
||||
"Monthly",
|
||||
employee=employee_doc.name,
|
||||
employee=applicant,
|
||||
company="_Test Company",
|
||||
currency=company_doc.default_currency,
|
||||
)
|
||||
@ -353,11 +290,110 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
self.assertEqual(row.principal_amount, principal_amount)
|
||||
self.assertEqual(row.total_payment, interest_amount + principal_amount)
|
||||
|
||||
if salary_slip.docstatus == 0:
|
||||
frappe.delete_doc("Salary Slip", name)
|
||||
def test_salary_slip_operation_queueing(self):
|
||||
company = "_Test Company"
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
employee = make_employee("test_employee@payroll.com", company=company)
|
||||
setup_salary_structure(employee, company_doc)
|
||||
|
||||
# enqueue salary slip creation via payroll entry
|
||||
# Payroll Entry status should change to Queued
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = get_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
company=company_doc.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
frappe.flags.enqueue_payroll_entry = True
|
||||
payroll_entry.submit()
|
||||
payroll_entry.reload()
|
||||
|
||||
self.assertEqual(payroll_entry.status, "Queued")
|
||||
frappe.flags.enqueue_payroll_entry = False
|
||||
|
||||
def test_salary_slip_operation_failure(self):
|
||||
company = "_Test Company"
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
employee = make_employee("test_employee@payroll.com", company=company)
|
||||
|
||||
salary_structure = make_salary_structure(
|
||||
"_Test Salary Structure",
|
||||
"Monthly",
|
||||
employee,
|
||||
company=company,
|
||||
currency=company_doc.default_currency,
|
||||
)
|
||||
|
||||
# reset account in component to test submission failure
|
||||
component = frappe.get_doc("Salary Component", salary_structure.earnings[0].salary_component)
|
||||
component.accounts = []
|
||||
component.save()
|
||||
|
||||
# salary slip submission via payroll entry
|
||||
# Payroll Entry status should change to Failed because of the missing account setup
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = get_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
company=company_doc.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
|
||||
# set employee as Inactive to check creation failure
|
||||
frappe.db.set_value("Employee", employee, "status", "Inactive")
|
||||
payroll_entry.submit()
|
||||
payroll_entry.reload()
|
||||
self.assertEqual(payroll_entry.status, "Failed")
|
||||
self.assertIsNotNone(payroll_entry.error_message)
|
||||
|
||||
frappe.db.set_value("Employee", employee, "status", "Active")
|
||||
payroll_entry.submit()
|
||||
payroll_entry.submit_salary_slips()
|
||||
|
||||
payroll_entry.reload()
|
||||
self.assertEqual(payroll_entry.status, "Failed")
|
||||
self.assertIsNotNone(payroll_entry.error_message)
|
||||
|
||||
# set accounts
|
||||
for data in frappe.get_all("Salary Component", pluck="name"):
|
||||
set_salary_component_account(data, company_list=[company])
|
||||
|
||||
# Payroll Entry successful, status should change to Submitted
|
||||
payroll_entry.submit_salary_slips()
|
||||
payroll_entry.reload()
|
||||
|
||||
self.assertEqual(payroll_entry.status, "Submitted")
|
||||
self.assertEqual(payroll_entry.error_message, "")
|
||||
|
||||
def test_payroll_entry_status(self):
|
||||
company = "_Test Company"
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
employee = make_employee("test_employee@payroll.com", company=company)
|
||||
|
||||
setup_salary_structure(employee, company_doc)
|
||||
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = get_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
company=company_doc.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
payroll_entry.submit()
|
||||
self.assertEqual(payroll_entry.status, "Submitted")
|
||||
|
||||
payroll_entry.cancel()
|
||||
self.assertEqual(payroll_entry.status, "Cancelled")
|
||||
|
||||
|
||||
def make_payroll_entry(**args):
|
||||
def get_payroll_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
payroll_entry = frappe.new_doc("Payroll Entry")
|
||||
@ -380,8 +416,17 @@ def make_payroll_entry(**args):
|
||||
payroll_entry.payment_account = args.payment_account
|
||||
|
||||
payroll_entry.fill_employee_details()
|
||||
payroll_entry.save()
|
||||
payroll_entry.create_salary_slips()
|
||||
payroll_entry.insert()
|
||||
|
||||
# Commit so that the first salary slip creation failure does not rollback the Payroll Entry insert.
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
return payroll_entry
|
||||
|
||||
|
||||
def make_payroll_entry(**args):
|
||||
payroll_entry = get_payroll_entry(**args)
|
||||
payroll_entry.submit()
|
||||
payroll_entry.submit_salary_slips()
|
||||
if payroll_entry.get_sal_slip_list(ss_status=1):
|
||||
payroll_entry.make_payment_entry()
|
||||
@ -423,10 +468,17 @@ def make_holiday(holiday_list_name):
|
||||
return holiday_list_name
|
||||
|
||||
|
||||
def get_salary_slip(user, period, salary_structure):
|
||||
salary_slip = make_employee_salary_slip(user, period, salary_structure)
|
||||
salary_slip.exchange_rate = 70
|
||||
salary_slip.calculate_net_pay()
|
||||
salary_slip.db_update()
|
||||
def setup_salary_structure(employee, company_doc, currency=None, salary_structure=None):
|
||||
for data in frappe.get_all("Salary Component", pluck="name"):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data, "company": company_doc.name}, "name"
|
||||
):
|
||||
set_salary_component_account(data)
|
||||
|
||||
return salary_slip
|
||||
make_salary_structure(
|
||||
salary_structure or "_Test Salary Structure",
|
||||
"Monthly",
|
||||
employee,
|
||||
company=company_doc.name,
|
||||
currency=(currency or company_doc.default_currency),
|
||||
)
|
||||
|
@ -29,6 +29,9 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||
calculate_amounts,
|
||||
create_repayment_entry,
|
||||
)
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_term_loans,
|
||||
)
|
||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||
get_benefit_component_amount,
|
||||
@ -116,10 +119,10 @@ class SalarySlip(TransactionBase):
|
||||
self.update_payment_status_for_gratuity()
|
||||
|
||||
def update_payment_status_for_gratuity(self):
|
||||
add_salary = frappe.db.get_all(
|
||||
additional_salary = frappe.db.get_all(
|
||||
"Additional Salary",
|
||||
filters={
|
||||
"payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
|
||||
"payroll_date": ("between", [self.start_date, self.end_date]),
|
||||
"employee": self.employee,
|
||||
"ref_doctype": "Gratuity",
|
||||
"docstatus": 1,
|
||||
@ -128,10 +131,10 @@ class SalarySlip(TransactionBase):
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if len(add_salary):
|
||||
if additional_salary:
|
||||
status = "Paid" if self.docstatus == 1 else "Unpaid"
|
||||
if add_salary[0].name in [data.additional_salary for data in self.earnings]:
|
||||
frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
|
||||
if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]:
|
||||
frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status)
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status()
|
||||
@ -462,37 +465,14 @@ class SalarySlip(TransactionBase):
|
||||
)
|
||||
|
||||
for d in range(working_days):
|
||||
dt = add_days(cstr(getdate(self.start_date)), d)
|
||||
leave = frappe.db.sql(
|
||||
"""
|
||||
SELECT t1.name,
|
||||
CASE WHEN (t1.half_day_date = %(dt)s or t1.to_date = t1.from_date)
|
||||
THEN t1.half_day else 0 END,
|
||||
t2.is_ppl,
|
||||
t2.fraction_of_daily_salary_per_leave
|
||||
FROM `tabLeave Application` t1, `tabLeave Type` t2
|
||||
WHERE t2.name = t1.leave_type
|
||||
AND (t2.is_lwp = 1 or t2.is_ppl = 1)
|
||||
AND t1.docstatus = 1
|
||||
AND t1.employee = %(employee)s
|
||||
AND ifnull(t1.salary_slip, '') = ''
|
||||
AND CASE
|
||||
WHEN t2.include_holiday != 1
|
||||
THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date
|
||||
WHEN t2.include_holiday
|
||||
THEN %(dt)s between from_date and to_date
|
||||
END
|
||||
""".format(
|
||||
holidays
|
||||
),
|
||||
{"employee": self.employee, "dt": dt},
|
||||
)
|
||||
date = add_days(cstr(getdate(self.start_date)), d)
|
||||
leave = get_lwp_or_ppl_for_date(date, self.employee, holidays)
|
||||
|
||||
if leave:
|
||||
equivalent_lwp_count = 0
|
||||
is_half_day_leave = cint(leave[0][1])
|
||||
is_partially_paid_leave = cint(leave[0][2])
|
||||
fraction_of_daily_salary_per_leave = flt(leave[0][3])
|
||||
is_half_day_leave = cint(leave[0].is_half_day)
|
||||
is_partially_paid_leave = cint(leave[0].is_ppl)
|
||||
fraction_of_daily_salary_per_leave = flt(leave[0].fraction_of_daily_salary_per_leave)
|
||||
|
||||
equivalent_lwp_count = (1 - daily_wages_fraction_for_half_day) if is_half_day_leave else 1
|
||||
|
||||
@ -1364,9 +1344,9 @@ class SalarySlip(TransactionBase):
|
||||
self.total_loan_repayment += payment.total_payment
|
||||
|
||||
def get_loan_details(self):
|
||||
return frappe.get_all(
|
||||
loan_details = frappe.get_all(
|
||||
"Loan",
|
||||
fields=["name", "interest_income_account", "loan_account", "loan_type"],
|
||||
fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
|
||||
filters={
|
||||
"applicant": self.employee,
|
||||
"docstatus": 1,
|
||||
@ -1375,6 +1355,15 @@ class SalarySlip(TransactionBase):
|
||||
},
|
||||
)
|
||||
|
||||
if loan_details:
|
||||
for loan in loan_details:
|
||||
if loan.is_term_loan:
|
||||
process_loan_interest_accrual_for_term_loans(
|
||||
posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name
|
||||
)
|
||||
|
||||
return loan_details
|
||||
|
||||
def make_loan_repayment_entry(self):
|
||||
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
||||
for loan in self.loans:
|
||||
@ -1730,3 +1719,46 @@ def eval_tax_slab_condition(condition, eval_globals=None, eval_locals=None):
|
||||
except Exception as e:
|
||||
frappe.throw(_("Error in formula or condition: {0} in Income Tax Slab").format(e))
|
||||
raise
|
||||
|
||||
|
||||
def get_lwp_or_ppl_for_date(date, employee, holidays):
|
||||
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||
LeaveType = frappe.qb.DocType("Leave Type")
|
||||
|
||||
is_half_day = (
|
||||
frappe.qb.terms.Case()
|
||||
.when(
|
||||
(
|
||||
(LeaveApplication.half_day_date == date)
|
||||
| (LeaveApplication.from_date == LeaveApplication.to_date)
|
||||
),
|
||||
LeaveApplication.half_day,
|
||||
)
|
||||
.else_(0)
|
||||
).as_("is_half_day")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(LeaveApplication)
|
||||
.inner_join(LeaveType)
|
||||
.on((LeaveType.name == LeaveApplication.leave_type))
|
||||
.select(
|
||||
LeaveApplication.name,
|
||||
LeaveType.is_ppl,
|
||||
LeaveType.fraction_of_daily_salary_per_leave,
|
||||
(is_half_day),
|
||||
)
|
||||
.where(
|
||||
(((LeaveType.is_lwp == 1) | (LeaveType.is_ppl == 1)))
|
||||
& (LeaveApplication.docstatus == 1)
|
||||
& (LeaveApplication.status == "Approved")
|
||||
& (LeaveApplication.employee == employee)
|
||||
& ((LeaveApplication.salary_slip.isnull()) | (LeaveApplication.salary_slip == ""))
|
||||
& ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date))
|
||||
)
|
||||
)
|
||||
|
||||
# if it's a holiday only include if leave type has "include holiday" enabled
|
||||
if date in holidays:
|
||||
query = query.where((LeaveType.include_holiday == "1"))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
@ -49,7 +49,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
"Payroll Settings", {"payroll_based_on": "Attendance", "daily_wages_fraction_for_half_day": 0.75}
|
||||
)
|
||||
def test_payment_days_based_on_attendance(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
|
||||
emp_id = make_employee("test_payment_days_based_on_attendance@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
@ -128,7 +128,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
},
|
||||
)
|
||||
def test_payment_days_for_mid_joinee_including_holidays(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
@ -196,7 +196,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
# tests mid month joining and relieving along with unmarked days
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
@ -236,7 +236,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
def test_payment_days_for_mid_joinee_excluding_holidays(self):
|
||||
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
month_start_date, month_end_date = get_first_day(nowdate()), get_last_day(nowdate())
|
||||
|
||||
new_emp_id = make_employee("test_payment_days_based_on_joining_date@salary.com")
|
||||
@ -267,7 +267,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
@change_settings("Payroll Settings", {"payroll_based_on": "Leave"})
|
||||
def test_payment_days_based_on_leave_application(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
|
||||
emp_id = make_employee("test_payment_days_based_on_leave_application@salary.com")
|
||||
frappe.db.set_value("Employee", emp_id, {"relieving_date": None, "status": "Active"})
|
||||
@ -366,7 +366,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
salary_slip.submit()
|
||||
salary_slip.reload()
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
days_in_month = no_of_days[0]
|
||||
no_of_holidays = no_of_days[1]
|
||||
|
||||
@ -441,7 +441,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 1})
|
||||
def test_salary_slip_with_holidays_included(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
make_employee("test_salary_slip_with_holidays_included@salary.com")
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
@ -473,7 +473,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
|
||||
@change_settings("Payroll Settings", {"include_holidays_in_total_working_days": 0})
|
||||
def test_salary_slip_with_holidays_excluded(self):
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
make_employee("test_salary_slip_with_holidays_excluded@salary.com")
|
||||
frappe.db.set_value(
|
||||
"Employee",
|
||||
@ -510,7 +510,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
create_salary_structure_assignment,
|
||||
)
|
||||
|
||||
no_of_days = self.get_no_of_days()
|
||||
no_of_days = get_no_of_days()
|
||||
|
||||
# set joinng date in the same month
|
||||
employee = make_employee("test_payment_days@salary.com")
|
||||
@ -984,20 +984,21 @@ class TestSalarySlip(unittest.TestCase):
|
||||
activity_type.wage_rate = 25
|
||||
activity_type.save()
|
||||
|
||||
def get_no_of_days(self):
|
||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
no_of_holidays_in_month = len(
|
||||
[
|
||||
1
|
||||
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
if i[6] != 0
|
||||
]
|
||||
)
|
||||
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
def get_no_of_days():
|
||||
no_of_days_in_month = calendar.monthrange(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
no_of_holidays_in_month = len(
|
||||
[
|
||||
1
|
||||
for i in calendar.monthcalendar(getdate(nowdate()).year, getdate(nowdate()).month)
|
||||
if i[6] != 0
|
||||
]
|
||||
)
|
||||
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
|
||||
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
if not salary_structure:
|
||||
@ -1008,7 +1009,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
)
|
||||
|
||||
salary_structure_doc = make_salary_structure(
|
||||
salary_structure, payroll_frequency, employee=employee.name, company=employee.company
|
||||
salary_structure,
|
||||
payroll_frequency,
|
||||
employee=employee.name,
|
||||
company=employee.company,
|
||||
from_date=posting_date,
|
||||
)
|
||||
salary_slip_name = frappe.db.get_value(
|
||||
"Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}
|
||||
@ -1018,7 +1023,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name)
|
||||
salary_slip.employee_name = employee.employee_name
|
||||
salary_slip.payroll_frequency = payroll_frequency
|
||||
salary_slip.posting_date = nowdate()
|
||||
salary_slip.posting_date = posting_date or nowdate()
|
||||
salary_slip.insert()
|
||||
else:
|
||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
|
||||
@ -1046,10 +1051,10 @@ def make_salary_component(salary_components, test_tax, company_list=None):
|
||||
doc.update(salary_component)
|
||||
doc.insert()
|
||||
|
||||
get_salary_component_account(doc, company_list)
|
||||
set_salary_component_account(doc, company_list)
|
||||
|
||||
|
||||
def get_salary_component_account(sal_comp, company_list=None):
|
||||
def set_salary_component_account(sal_comp, company_list=None):
|
||||
company = erpnext.get_default_company()
|
||||
|
||||
if company_list and company not in company_list:
|
||||
@ -1132,6 +1137,7 @@ def make_earning_salary_component(
|
||||
"pay_against_benefit_claim": 0,
|
||||
"type": "Earning",
|
||||
"max_benefit_amount": 15000,
|
||||
"depends_on_payment_days": 1,
|
||||
},
|
||||
]
|
||||
)
|
||||
|
@ -253,6 +253,7 @@ def make_salary_slip(
|
||||
source_name,
|
||||
target_doc=None,
|
||||
employee=None,
|
||||
posting_date=None,
|
||||
as_print=False,
|
||||
print_format=None,
|
||||
for_preview=0,
|
||||
@ -269,6 +270,9 @@ def make_salary_slip(
|
||||
target.designation = employee_details.designation
|
||||
target.department = employee_details.department
|
||||
|
||||
if posting_date:
|
||||
target.posting_date = posting_date
|
||||
|
||||
target.run_method("process_salary_structure", for_preview=for_preview)
|
||||
|
||||
doc = get_mapped_doc(
|
||||
|
@ -169,9 +169,6 @@ def make_salary_structure(
|
||||
payroll_period=None,
|
||||
include_flexi_benefits=False,
|
||||
):
|
||||
if test_tax:
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||
|
||||
if frappe.db.exists("Salary Structure", salary_structure):
|
||||
frappe.db.delete("Salary Structure", salary_structure)
|
||||
|
||||
@ -230,9 +227,12 @@ def create_salary_structure_assignment(
|
||||
company=None,
|
||||
currency=erpnext.get_default_currency(),
|
||||
payroll_period=None,
|
||||
base=None,
|
||||
allow_duplicate=False,
|
||||
):
|
||||
|
||||
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
|
||||
if not allow_duplicate and frappe.db.exists(
|
||||
"Salary Structure Assignment", {"employee": employee}
|
||||
):
|
||||
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
|
||||
|
||||
if not payroll_period:
|
||||
@ -245,7 +245,7 @@ def create_salary_structure_assignment(
|
||||
|
||||
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
||||
salary_structure_assignment.employee = employee
|
||||
salary_structure_assignment.base = 50000
|
||||
salary_structure_assignment.base = base or 50000
|
||||
salary_structure_assignment.variable = 5000
|
||||
|
||||
if not from_date:
|
||||
|
@ -234,7 +234,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "actual_start_date",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Date",
|
||||
"label": "Actual Start Date (via Time Sheet)",
|
||||
"read_only": 1
|
||||
},
|
||||
@ -458,7 +458,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"max_attachments": 4,
|
||||
"modified": "2022-01-29 13:58:27.712714",
|
||||
"modified": "2022-05-25 22:45:06.108499",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Project",
|
||||
@ -504,4 +504,4 @@
|
||||
"timeline_field": "customer",
|
||||
"title_field": "project_name",
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ erpnext.buying.BuyingController = class BuyingController extends erpnext.Transac
|
||||
me.frm.set_query('supplier_address', erpnext.queries.address_query);
|
||||
|
||||
me.frm.set_query('billing_address', erpnext.queries.company_address_query);
|
||||
erpnext.accounts.dimensions.setup_dimension_filters(me.frm, me.frm.doctype);
|
||||
|
||||
if(this.frm.fields_dict.supplier) {
|
||||
this.frm.set_query("supplier", function() {
|
||||
|
@ -789,11 +789,23 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
|
||||
if(this.frm.doc.is_pos && (update_paid_amount===undefined || update_paid_amount)) {
|
||||
$.each(this.frm.doc['payments'] || [], function(index, data) {
|
||||
if(data.default && payment_status && total_amount_to_pay > 0) {
|
||||
let base_amount = flt(total_amount_to_pay, precision("base_amount", data));
|
||||
let base_amount, amount;
|
||||
|
||||
if (me.frm.doc.party_account_currency == me.frm.doc.currency) {
|
||||
// if customer/supplier currency is same as company currency
|
||||
// total_amount_to_pay is already in customer/supplier currency
|
||||
// so base_amount has to be calculated using total_amount_to_pay
|
||||
base_amount = flt(total_amount_to_pay * me.frm.doc.conversion_rate, precision("base_amount", data));
|
||||
amount = flt(total_amount_to_pay, precision("amount", data));
|
||||
} else {
|
||||
base_amount = flt(total_amount_to_pay, precision("base_amount", data));
|
||||
amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
|
||||
}
|
||||
|
||||
frappe.model.set_value(data.doctype, data.name, "base_amount", base_amount);
|
||||
let amount = flt(total_amount_to_pay / me.frm.doc.conversion_rate, precision("amount", data));
|
||||
frappe.model.set_value(data.doctype, data.name, "amount", amount);
|
||||
payment_status = false;
|
||||
|
||||
} else if(me.frm.doc.paid_amount) {
|
||||
frappe.model.set_value(data.doctype, data.name, "amount", 0.0);
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user