mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-18 20:49:19 +00:00
Merge branch 'develop' into fix-reserve-qty
# Conflicts: # erpnext/patches.txt # erpnext/public/js/utils/barcode_scanner.js # erpnext/regional/report/gstr_1/gstr_1.py # erpnext/stock/doctype/delivery_note/test_delivery_note.py
This commit is contained in:
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@@ -11,7 +11,7 @@ fi
|
|||||||
|
|
||||||
cd ~ || exit
|
cd ~ || exit
|
||||||
|
|
||||||
sudo apt-get install redis-server libcups2-dev
|
sudo apt update && sudo apt install redis-server libcups2-dev
|
||||||
|
|
||||||
pip install frappe-bench
|
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
|
:) Also, even if it is closed, you can always reopen the PR when you're
|
||||||
ready. Thank you for contributing.
|
ready. Thank you for contributing.
|
||||||
|
|
||||||
issues:
|
only: pulls
|
||||||
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.
|
|
||||||
|
|||||||
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_payment_entries(self)
|
||||||
unlink_ref_doc_from_salary_slip(self.name)
|
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.make_gl_entries(1)
|
||||||
self.update_advance_paid()
|
self.update_advance_paid()
|
||||||
self.update_expense_claim()
|
self.update_expense_claim()
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class PaymentEntry(AccountsController):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
|
|
||||||
def on_cancel(self):
|
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.make_gl_entries(cancel=1)
|
||||||
self.update_expense_claim()
|
self.update_expense_claim()
|
||||||
self.update_outstanding_amounts()
|
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,178 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"allow_rename": 1,
|
||||||
|
"creation": "2022-05-09 19:35:03.334361",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"editable_grid": 1,
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"posting_date",
|
||||||
|
"company",
|
||||||
|
"account_type",
|
||||||
|
"account",
|
||||||
|
"party_type",
|
||||||
|
"party",
|
||||||
|
"due_date",
|
||||||
|
"cost_center",
|
||||||
|
"finance_book",
|
||||||
|
"voucher_type",
|
||||||
|
"voucher_no",
|
||||||
|
"against_voucher_type",
|
||||||
|
"against_voucher_no",
|
||||||
|
"amount",
|
||||||
|
"account_currency",
|
||||||
|
"amount_in_account_currency",
|
||||||
|
"delinked"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "posting_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Posting Date"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account_type",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"label": "Account Type",
|
||||||
|
"options": "Receivable\nPayable"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "account",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"label": "Account",
|
||||||
|
"options": "Account"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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-30 19:04:55.532171",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Payment Ledger Entry",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts User",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Accounts Manager",
|
||||||
|
"share": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"report": 1,
|
||||||
|
"role": "Auditor",
|
||||||
|
"share": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"search_fields": "voucher_no, against_voucher_no",
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -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])
|
||||||
@@ -54,8 +54,8 @@ class PeriodClosingVoucher(AccountsController):
|
|||||||
|
|
||||||
pce = frappe.db.sql(
|
pce = frappe.db.sql(
|
||||||
"""select name from `tabPeriod Closing Voucher`
|
"""select name from `tabPeriod Closing Voucher`
|
||||||
where posting_date > %s and fiscal_year = %s and docstatus = 1""",
|
where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""",
|
||||||
(self.posting_date, self.fiscal_year),
|
(self.posting_date, self.fiscal_year, self.company),
|
||||||
)
|
)
|
||||||
if pce and pce[0][0]:
|
if pce and pce[0][0]:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
|
|||||||
@@ -78,6 +78,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
expense_account="Cost of Goods Sold - TPC",
|
expense_account="Cost of Goods Sold - TPC",
|
||||||
rate=400,
|
rate=400,
|
||||||
debit_to="Debtors - TPC",
|
debit_to="Debtors - TPC",
|
||||||
|
currency="USD",
|
||||||
|
customer="_Test Customer USD",
|
||||||
)
|
)
|
||||||
create_sales_invoice(
|
create_sales_invoice(
|
||||||
company=company,
|
company=company,
|
||||||
@@ -86,6 +88,8 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
expense_account="Cost of Goods Sold - TPC",
|
expense_account="Cost of Goods Sold - TPC",
|
||||||
rate=200,
|
rate=200,
|
||||||
debit_to="Debtors - TPC",
|
debit_to="Debtors - TPC",
|
||||||
|
currency="USD",
|
||||||
|
customer="_Test Customer USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
pcv = self.make_period_closing_voucher(submit=False)
|
pcv = self.make_period_closing_voucher(submit=False)
|
||||||
@@ -119,14 +123,17 @@ class TestPeriodClosingVoucher(unittest.TestCase):
|
|||||||
surplus_account = create_account()
|
surplus_account = create_account()
|
||||||
cost_center = create_cost_center("Test Cost Center 1")
|
cost_center = create_cost_center("Test Cost Center 1")
|
||||||
|
|
||||||
create_sales_invoice(
|
si = create_sales_invoice(
|
||||||
company=company,
|
company=company,
|
||||||
income_account="Sales - TPC",
|
income_account="Sales - TPC",
|
||||||
expense_account="Cost of Goods Sold - TPC",
|
expense_account="Cost of Goods Sold - TPC",
|
||||||
cost_center=cost_center,
|
cost_center=cost_center,
|
||||||
rate=400,
|
rate=400,
|
||||||
debit_to="Debtors - TPC",
|
debit_to="Debtors - TPC",
|
||||||
|
currency="USD",
|
||||||
|
customer="_Test Customer USD",
|
||||||
)
|
)
|
||||||
|
|
||||||
jv = make_journal_entry(
|
jv = make_journal_entry(
|
||||||
account1="Cash - TPC",
|
account1="Cash - TPC",
|
||||||
account2="Sales - 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("grand_total", 0);
|
||||||
frm.set_value("net_total", 0);
|
frm.set_value("net_total", 0);
|
||||||
frm.set_value("total_quantity", 0);
|
frm.set_value("total_quantity", 0);
|
||||||
@@ -112,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', {
|
|||||||
row.expected_amount = row.opening_amount;
|
row.expected_amount = row.opening_amount;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (let row of frm.doc.pos_transactions) {
|
const pos_inv_promises = frm.doc.pos_transactions.map(
|
||||||
frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
|
row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
|
||||||
frm.doc.grand_total += flt(doc.grand_total);
|
);
|
||||||
frm.doc.net_total += flt(doc.net_total);
|
|
||||||
frm.doc.total_quantity += flt(doc.total_qty);
|
const pos_invoices = await Promise.all(pos_inv_promises);
|
||||||
refresh_payments(doc, frm);
|
|
||||||
refresh_taxes(doc, frm);
|
for (let doc of pos_invoices) {
|
||||||
refresh_fields(frm);
|
frm.doc.grand_total += flt(doc.grand_total);
|
||||||
set_html_data(frm);
|
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):
|
def on_cancel(self):
|
||||||
|
self.ignore_linked_doctypes = "Payment Ledger Entry"
|
||||||
# run on cancel method of selling controller
|
# run on cancel method of selling controller
|
||||||
super(SalesInvoice, self).on_cancel()
|
super(SalesInvoice, self).on_cancel()
|
||||||
if not self.is_return and self.loyalty_program:
|
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",
|
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 = si.items[0]
|
||||||
item.stock_qty = 1
|
item.stock_qty = 1
|
||||||
si.save()
|
si.save()
|
||||||
|
|||||||
@@ -45,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying.
|
|||||||
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
|
||||||
this.frm.trigger('supplier');
|
this.frm.trigger('supplier');
|
||||||
}
|
}
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(doc) {
|
refresh(doc) {
|
||||||
|
|||||||
@@ -545,7 +545,16 @@ class PurchaseInvoice(BuyingController):
|
|||||||
from_repost=from_repost,
|
from_repost=from_repost,
|
||||||
)
|
)
|
||||||
elif self.docstatus == 2:
|
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)
|
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":
|
if update_outstanding == "No":
|
||||||
update_outstanding_amt(
|
update_outstanding_amt(
|
||||||
@@ -1127,7 +1136,7 @@ class PurchaseInvoice(BuyingController):
|
|||||||
# Stock ledger value is not matching with the warehouse amount
|
# Stock ledger value is not matching with the warehouse amount
|
||||||
if (
|
if (
|
||||||
self.update_stock
|
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
|
and warehouse_debit_amount
|
||||||
!= flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
|
!= 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")
|
frappe.db.set(self, "status", "Cancelled")
|
||||||
|
|
||||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
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)
|
self.update_advance_tax_references(cancel=1)
|
||||||
|
|
||||||
def update_project(self):
|
def update_project(self):
|
||||||
|
|||||||
@@ -27,12 +27,13 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
|
|||||||
make_purchase_receipt,
|
make_purchase_receipt,
|
||||||
)
|
)
|
||||||
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
|
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_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
|
||||||
test_ignore = ["Serial No"]
|
test_ignore = ["Serial No"]
|
||||||
|
|
||||||
|
|
||||||
class TestPurchaseInvoice(unittest.TestCase):
|
class TestPurchaseInvoice(unittest.TestCase, StockTestMixin):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(self):
|
def setUpClass(self):
|
||||||
unlink_payment_on_cancel_of_invoice()
|
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][0], gle.debit)
|
||||||
self.assertEqual(expected_values[gle.account][1], gle.credit)
|
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):
|
def test_multi_currency_gle(self):
|
||||||
pi = make_purchase_invoice(
|
pi = make_purchase_invoice(
|
||||||
supplier="_Test Supplier USD",
|
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)
|
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.enable_provisional_accounting_for_non_stock_items = 0
|
||||||
company.save()
|
company.save()
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2013-01-10 16:34:08",
|
"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.",
|
"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",
|
"doctype": "DocType",
|
||||||
"document_type": "Setup",
|
"document_type": "Setup",
|
||||||
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
"title",
|
"title",
|
||||||
"is_default",
|
"is_default",
|
||||||
@@ -74,7 +76,8 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-money",
|
"icon": "fa fa-money",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"modified": "2019-11-25 13:05:26.220275",
|
"links": [],
|
||||||
|
"modified": "2022-05-16 16:15:29.059370",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Purchase Taxes and Charges Template",
|
"name": "Purchase Taxes and Charges Template",
|
||||||
@@ -103,6 +106,10 @@
|
|||||||
"role": "Purchase User"
|
"role": "Purchase User"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"show_title_field_in_link": 1,
|
||||||
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -52,7 +52,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
|||||||
me.frm.refresh_fields();
|
me.frm.refresh_fields();
|
||||||
}
|
}
|
||||||
erpnext.queries.setup_warehouse_query(this.frm);
|
erpnext.queries.setup_warehouse_query(this.frm);
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
refresh(doc, dt, dn) {
|
refresh(doc, dt, dn) {
|
||||||
@@ -861,27 +860,44 @@ frappe.ui.form.on('Sales Invoice', {
|
|||||||
|
|
||||||
set_timesheet_data: function(frm, timesheets) {
|
set_timesheet_data: function(frm, timesheets) {
|
||||||
frm.clear_table("timesheets")
|
frm.clear_table("timesheets")
|
||||||
timesheets.forEach(timesheet => {
|
timesheets.forEach(async (timesheet) => {
|
||||||
if (frm.doc.currency != timesheet.currency) {
|
if (frm.doc.currency != timesheet.currency) {
|
||||||
frappe.call({
|
const exchange_rate = await frm.events.get_exchange_rate(
|
||||||
method: "erpnext.setup.utils.get_exchange_rate",
|
frm, timesheet.currency, frm.doc.currency
|
||||||
args: {
|
)
|
||||||
from_currency: timesheet.currency,
|
frm.events.append_time_log(frm, timesheet, exchange_rate)
|
||||||
to_currency: frm.doc.currency
|
|
||||||
},
|
|
||||||
callback: function(r) {
|
|
||||||
if (r.message) {
|
|
||||||
exchange_rate = r.message;
|
|
||||||
frm.events.append_time_log(frm, timesheet, exchange_rate);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
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) {
|
append_time_log: function(frm, time_log, exchange_rate) {
|
||||||
const row = frm.add_child("timesheets");
|
const row = frm.add_child("timesheets");
|
||||||
row.activity_type = time_log.activity_type;
|
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_hours = time_log.billing_hours;
|
||||||
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
||||||
row.timesheet_detail = time_log.name;
|
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.refresh_field("timesheets");
|
||||||
frm.trigger("calculate_timesheet_totals");
|
frm.trigger("calculate_timesheet_totals");
|
||||||
|
|||||||
@@ -1790,6 +1790,8 @@
|
|||||||
"width": "50%"
|
"width": "50%"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
"fetch_from": "sales_partner.commission_rate",
|
||||||
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "commission_rate",
|
"fieldname": "commission_rate",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
"hide_days": 1,
|
"hide_days": 1,
|
||||||
@@ -2038,7 +2040,7 @@
|
|||||||
"link_fieldname": "consolidated_invoice"
|
"link_fieldname": "consolidated_invoice"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-03-08 16:08:53.517903",
|
"modified": "2022-06-10 03:52:51.409913",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Invoice",
|
"name": "Sales Invoice",
|
||||||
|
|||||||
@@ -396,7 +396,12 @@ class SalesInvoice(SellingController):
|
|||||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||||
|
|
||||||
self.unlink_sales_invoice_from_timesheets()
|
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):
|
def update_status_updater_args(self):
|
||||||
if cint(self.update_stock):
|
if cint(self.update_stock):
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"allow_import": 1,
|
"allow_import": 1,
|
||||||
"allow_rename": 1,
|
"allow_rename": 1,
|
||||||
"creation": "2013-01-10 16:34:09",
|
"creation": "2013-01-10 16:34:09",
|
||||||
@@ -77,7 +78,8 @@
|
|||||||
],
|
],
|
||||||
"icon": "fa fa-money",
|
"icon": "fa fa-money",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"modified": "2019-11-25 13:06:03.279099",
|
"links": [],
|
||||||
|
"modified": "2022-05-16 16:14:52.061672",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Sales Taxes and Charges Template",
|
"name": "Sales Taxes and Charges Template",
|
||||||
@@ -113,7 +115,10 @@
|
|||||||
"write": 1
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"show_title_field_in_link": 1,
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "ASC",
|
"sort_order": "ASC",
|
||||||
|
"states": [],
|
||||||
|
"title_field": "title",
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
|
|||||||
get_accounting_dimensions,
|
get_accounting_dimensions,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
|
||||||
|
from erpnext.accounts.utils import create_payment_ledger_entry
|
||||||
|
|
||||||
|
|
||||||
class ClosedAccountingPeriod(frappe.ValidationError):
|
class ClosedAccountingPeriod(frappe.ValidationError):
|
||||||
@@ -34,6 +35,7 @@ def make_gl_entries(
|
|||||||
validate_disabled_accounts(gl_map)
|
validate_disabled_accounts(gl_map)
|
||||||
gl_map = process_gl_map(gl_map, merge_entries)
|
gl_map = process_gl_map(gl_map, merge_entries)
|
||||||
if gl_map and len(gl_map) > 1:
|
if gl_map and len(gl_map) > 1:
|
||||||
|
create_payment_ledger_entry(gl_map)
|
||||||
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
save_entries(gl_map, adv_adj, update_outstanding, from_repost)
|
||||||
# Post GL Map proccess there may no be any GL Entries
|
# Post GL Map proccess there may no be any GL Entries
|
||||||
elif gl_map:
|
elif gl_map:
|
||||||
@@ -479,6 +481,7 @@ def make_reverse_gl_entries(
|
|||||||
).run(as_dict=1)
|
).run(as_dict=1)
|
||||||
|
|
||||||
if gl_entries:
|
if gl_entries:
|
||||||
|
create_payment_ledger_entry(gl_entries, cancel=1)
|
||||||
validate_accounting_period(gl_entries)
|
validate_accounting_period(gl_entries)
|
||||||
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
check_freezing_date(gl_entries[0]["posting_date"], adv_adj)
|
||||||
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"])
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
|
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"modified": "2022-01-18 18:35:52.326688",
|
"modified": "2022-06-07 14:29:21.352132",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounts",
|
"name": "Accounts",
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
{
|
{
|
||||||
"action": "Watch Video",
|
"action": "Go to Page",
|
||||||
"action_label": "Learn more about Chart of Accounts",
|
"action_label": "Learn more about Chart of Accounts",
|
||||||
"callback_message": "You can continue with the onboarding after exploring this page",
|
"callback_message": "You can continue with the onboarding after exploring this page",
|
||||||
"callback_title": "Awesome Work",
|
"callback_title": "Explore Chart of Accounts",
|
||||||
"creation": "2020-05-13 19:58:20.928127",
|
"creation": "2020-05-13 19:58:20.928127",
|
||||||
"description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
|
"description": "# Chart Of Accounts\n\nERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to business and legal requirements.",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2021-08-13 11:46:25.878506",
|
"modified": "2022-06-07 14:21:26.264769",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Chart of Accounts",
|
"name": "Chart of Accounts",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -2,14 +2,14 @@
|
|||||||
"action": "Create Entry",
|
"action": "Create Entry",
|
||||||
"action_label": "Manage Sales Tax Templates",
|
"action_label": "Manage Sales Tax Templates",
|
||||||
"creation": "2020-05-13 19:29:43.844463",
|
"creation": "2020-05-13 19:29:43.844463",
|
||||||
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n",
|
"description": "# Setting up Taxes\n\nERPNext lets you configure your taxes so that they are automatically applied in your buying and selling transactions. You can configure them globally or even on Items. ERPNext taxes are pre-configured for most regions.\n\n[Checkout pre-configured taxes](/app/sales-taxes-and-charges-template)\n",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Onboarding Step",
|
"doctype": "Onboarding Step",
|
||||||
"idx": 0,
|
"idx": 0,
|
||||||
"is_complete": 0,
|
"is_complete": 0,
|
||||||
"is_single": 0,
|
"is_single": 0,
|
||||||
"is_skipped": 0,
|
"is_skipped": 0,
|
||||||
"modified": "2021-08-13 11:48:37.238610",
|
"modified": "2022-06-07 14:27:15.906286",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"name": "Setup Taxes",
|
"name": "Setup Taxes",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
|
|||||||
@@ -897,3 +897,18 @@ def get_default_contact(doctype, name):
|
|||||||
return None
|
return None
|
||||||
else:
|
else:
|
||||||
return None
|
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()
|
||||||
|
|||||||
@@ -42,7 +42,7 @@
|
|||||||
|
|
||||||
{% if(filters.show_future_payments) { %}
|
{% if(filters.show_future_payments) { %}
|
||||||
{% var balance_row = data.slice(-1).pop();
|
{% var balance_row = data.slice(-1).pop();
|
||||||
var start = filters.based_on_payment_terms ? 13 : 11;
|
var start = report.columns.findIndex((elem) => (elem.fieldname == 'age'));
|
||||||
var range1 = report.columns[start].label;
|
var range1 = report.columns[start].label;
|
||||||
var range2 = report.columns[start+1].label;
|
var range2 = report.columns[start+1].label;
|
||||||
var range3 = report.columns[start+2].label;
|
var range3 = report.columns[start+2].label;
|
||||||
|
|||||||
@@ -198,10 +198,12 @@ def get_loan_entries(filters):
|
|||||||
amount_field = (loan_doc.disbursed_amount).as_("credit")
|
amount_field = (loan_doc.disbursed_amount).as_("credit")
|
||||||
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
||||||
account = loan_doc.disbursement_account
|
account = loan_doc.disbursement_account
|
||||||
|
salary_condition = loan_doc.docstatus == 1
|
||||||
else:
|
else:
|
||||||
amount_field = (loan_doc.amount_paid).as_("debit")
|
amount_field = (loan_doc.amount_paid).as_("debit")
|
||||||
posting_date = (loan_doc.posting_date).as_("posting_date")
|
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||||
account = loan_doc.payment_account
|
account = loan_doc.payment_account
|
||||||
|
salary_condition = loan_doc.repay_from_salary == 0
|
||||||
|
|
||||||
query = (
|
query = (
|
||||||
frappe.qb.from_(loan_doc)
|
frappe.qb.from_(loan_doc)
|
||||||
@@ -214,14 +216,12 @@ def get_loan_entries(filters):
|
|||||||
posting_date,
|
posting_date,
|
||||||
)
|
)
|
||||||
.where(loan_doc.docstatus == 1)
|
.where(loan_doc.docstatus == 1)
|
||||||
|
.where(salary_condition)
|
||||||
.where(account == filters.get("account"))
|
.where(account == filters.get("account"))
|
||||||
.where(posting_date <= getdate(filters.get("report_date")))
|
.where(posting_date <= getdate(filters.get("report_date")))
|
||||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") > 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)
|
entries = query.run(as_dict=1)
|
||||||
loan_docs.extend(entries)
|
loan_docs.extend(entries)
|
||||||
|
|
||||||
@@ -267,15 +267,17 @@ def get_loan_amount(filters):
|
|||||||
amount_field = Sum(loan_doc.disbursed_amount)
|
amount_field = Sum(loan_doc.disbursed_amount)
|
||||||
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
posting_date = (loan_doc.disbursement_date).as_("posting_date")
|
||||||
account = loan_doc.disbursement_account
|
account = loan_doc.disbursement_account
|
||||||
|
salary_condition = loan_doc.docstatus == 1
|
||||||
else:
|
else:
|
||||||
amount_field = Sum(loan_doc.amount_paid)
|
amount_field = Sum(loan_doc.amount_paid)
|
||||||
posting_date = (loan_doc.posting_date).as_("posting_date")
|
posting_date = (loan_doc.posting_date).as_("posting_date")
|
||||||
account = loan_doc.payment_account
|
account = loan_doc.payment_account
|
||||||
|
salary_condition = loan_doc.repay_from_salary == 0
|
||||||
amount = (
|
amount = (
|
||||||
frappe.qb.from_(loan_doc)
|
frappe.qb.from_(loan_doc)
|
||||||
.select(amount_field)
|
.select(amount_field)
|
||||||
.where(loan_doc.docstatus == 1)
|
.where(loan_doc.docstatus == 1)
|
||||||
|
.where(salary_condition)
|
||||||
.where(account == filters.get("account"))
|
.where(account == filters.get("account"))
|
||||||
.where(posting_date > getdate(filters.get("report_date")))
|
.where(posting_date > getdate(filters.get("report_date")))
|
||||||
.where(ifnull(loan_doc.clearance_date, "4000-01-01") <= 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):
|
def get_chart_data(columns, data):
|
||||||
labels = [d.get("label") for d in columns[2:]]
|
labels = [d.get("label") for d in columns[2:]]
|
||||||
datasets = [
|
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
|
for account in data
|
||||||
if account.get("parent_account") == None and account.get("currency")
|
if account.get("parent_account") == None and account.get("currency")
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -50,7 +50,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Fiscal Year",
|
"options": "Fiscal Year",
|
||||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
on_change: () => {
|
||||||
|
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
|
||||||
|
let year_start_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), "year_start_date");
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_start_date: year_start_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"to_fiscal_year",
|
"fieldname":"to_fiscal_year",
|
||||||
@@ -58,7 +66,15 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
|||||||
"fieldtype": "Link",
|
"fieldtype": "Link",
|
||||||
"options": "Fiscal Year",
|
"options": "Fiscal Year",
|
||||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
"reqd": 1
|
"reqd": 1,
|
||||||
|
on_change: () => {
|
||||||
|
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
|
||||||
|
let year_end_date = frappe.model.get_value("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), "year_end_date");
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_end_date: year_end_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"fieldname":"finance_book",
|
"fieldname":"finance_book",
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ frappe.query_reports["Gross Profit"] = {
|
|||||||
"fieldname":"group_by",
|
"fieldname":"group_by",
|
||||||
"label": __("Group By"),
|
"label": __("Group By"),
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject",
|
"options": "Invoice\nItem Code\nItem Group\nBrand\nWarehouse\nCustomer\nCustomer Group\nTerritory\nSales Person\nProject\nMonthly\nPayment Term",
|
||||||
"default": "Invoice"
|
"default": "Invoice"
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, scrub
|
from frappe import _, scrub
|
||||||
from frappe.utils import cint, flt
|
from frappe.utils import cint, flt, formatdate
|
||||||
|
|
||||||
from erpnext.controllers.queries import get_match_cond
|
from erpnext.controllers.queries import get_match_cond
|
||||||
from erpnext.stock.utils import get_incoming_rate
|
from erpnext.stock.utils import get_incoming_rate
|
||||||
@@ -124,6 +124,23 @@ def execute(filters=None):
|
|||||||
"gross_profit",
|
"gross_profit",
|
||||||
"gross_profit_percent",
|
"gross_profit_percent",
|
||||||
],
|
],
|
||||||
|
"monthly": [
|
||||||
|
"monthly",
|
||||||
|
"qty",
|
||||||
|
"base_rate",
|
||||||
|
"buying_rate",
|
||||||
|
"base_amount",
|
||||||
|
"buying_amount",
|
||||||
|
"gross_profit",
|
||||||
|
"gross_profit_percent",
|
||||||
|
],
|
||||||
|
"payment_term": [
|
||||||
|
"payment_term",
|
||||||
|
"base_amount",
|
||||||
|
"buying_amount",
|
||||||
|
"gross_profit",
|
||||||
|
"gross_profit_percent",
|
||||||
|
],
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -317,6 +334,19 @@ def get_columns(group_wise_columns, filters):
|
|||||||
"options": "territory",
|
"options": "territory",
|
||||||
"width": 100,
|
"width": 100,
|
||||||
},
|
},
|
||||||
|
"monthly": {
|
||||||
|
"label": _("Monthly"),
|
||||||
|
"fieldname": "monthly",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"width": 100,
|
||||||
|
},
|
||||||
|
"payment_term": {
|
||||||
|
"label": _("Payment Term"),
|
||||||
|
"fieldname": "payment_term",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Payment Term",
|
||||||
|
"width": 170,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -390,6 +420,9 @@ class GrossProfitGenerator(object):
|
|||||||
buying_amount = 0
|
buying_amount = 0
|
||||||
|
|
||||||
for row in reversed(self.si_list):
|
for row in reversed(self.si_list):
|
||||||
|
if self.filters.get("group_by") == "Monthly":
|
||||||
|
row.monthly = formatdate(row.posting_date, "MMM YYYY")
|
||||||
|
|
||||||
if self.skip_row(row):
|
if self.skip_row(row):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -445,17 +478,7 @@ class GrossProfitGenerator(object):
|
|||||||
|
|
||||||
def get_average_rate_based_on_group_by(self):
|
def get_average_rate_based_on_group_by(self):
|
||||||
for key in list(self.grouped):
|
for key in list(self.grouped):
|
||||||
if self.filters.get("group_by") != "Invoice":
|
if self.filters.get("group_by") == "Invoice":
|
||||||
for i, row in enumerate(self.grouped[key]):
|
|
||||||
if i == 0:
|
|
||||||
new_row = row
|
|
||||||
else:
|
|
||||||
new_row.qty += flt(row.qty)
|
|
||||||
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
|
||||||
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
|
||||||
new_row = self.set_average_rate(new_row)
|
|
||||||
self.grouped_data.append(new_row)
|
|
||||||
else:
|
|
||||||
for i, row in enumerate(self.grouped[key]):
|
for i, row in enumerate(self.grouped[key]):
|
||||||
if row.indent == 1.0:
|
if row.indent == 1.0:
|
||||||
if (
|
if (
|
||||||
@@ -469,6 +492,44 @@ class GrossProfitGenerator(object):
|
|||||||
if flt(row.qty) or row.base_amount:
|
if flt(row.qty) or row.base_amount:
|
||||||
row = self.set_average_rate(row)
|
row = self.set_average_rate(row)
|
||||||
self.grouped_data.append(row)
|
self.grouped_data.append(row)
|
||||||
|
elif self.filters.get("group_by") == "Payment Term":
|
||||||
|
for i, row in enumerate(self.grouped[key]):
|
||||||
|
invoice_portion = 0
|
||||||
|
|
||||||
|
if row.is_return:
|
||||||
|
invoice_portion = 100
|
||||||
|
elif row.invoice_portion:
|
||||||
|
invoice_portion = row.invoice_portion
|
||||||
|
else:
|
||||||
|
invoice_portion = row.payment_amount * 100 / row.base_net_amount
|
||||||
|
|
||||||
|
if i == 0:
|
||||||
|
new_row = row
|
||||||
|
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion)
|
||||||
|
else:
|
||||||
|
new_row.qty += flt(row.qty)
|
||||||
|
self.set_average_based_on_payment_term_portion(new_row, row, invoice_portion, True)
|
||||||
|
|
||||||
|
new_row = self.set_average_rate(new_row)
|
||||||
|
self.grouped_data.append(new_row)
|
||||||
|
else:
|
||||||
|
for i, row in enumerate(self.grouped[key]):
|
||||||
|
if i == 0:
|
||||||
|
new_row = row
|
||||||
|
else:
|
||||||
|
new_row.qty += flt(row.qty)
|
||||||
|
new_row.buying_amount += flt(row.buying_amount, self.currency_precision)
|
||||||
|
new_row.base_amount += flt(row.base_amount, self.currency_precision)
|
||||||
|
new_row = self.set_average_rate(new_row)
|
||||||
|
self.grouped_data.append(new_row)
|
||||||
|
|
||||||
|
def set_average_based_on_payment_term_portion(self, new_row, row, invoice_portion, aggr=False):
|
||||||
|
cols = ["base_amount", "buying_amount", "gross_profit"]
|
||||||
|
for col in cols:
|
||||||
|
if aggr:
|
||||||
|
new_row[col] += row[col] * invoice_portion / 100
|
||||||
|
else:
|
||||||
|
new_row[col] = row[col] * invoice_portion / 100
|
||||||
|
|
||||||
def is_not_invoice_row(self, row):
|
def is_not_invoice_row(self, row):
|
||||||
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
|
return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get(
|
||||||
@@ -622,6 +683,20 @@ class GrossProfitGenerator(object):
|
|||||||
sales_person_cols = ""
|
sales_person_cols = ""
|
||||||
sales_team_table = ""
|
sales_team_table = ""
|
||||||
|
|
||||||
|
if self.filters.group_by == "Payment Term":
|
||||||
|
payment_term_cols = """,if(`tabSales Invoice`.is_return = 1,
|
||||||
|
'{0}',
|
||||||
|
coalesce(schedule.payment_term, '{1}')) as payment_term,
|
||||||
|
schedule.invoice_portion,
|
||||||
|
schedule.payment_amount """.format(
|
||||||
|
_("Sales Return"), _("No Terms")
|
||||||
|
)
|
||||||
|
payment_term_table = """ left join `tabPayment Schedule` schedule on schedule.parent = `tabSales Invoice`.name and
|
||||||
|
`tabSales Invoice`.is_return = 0 """
|
||||||
|
else:
|
||||||
|
payment_term_cols = ""
|
||||||
|
payment_term_table = ""
|
||||||
|
|
||||||
if self.filters.get("sales_invoice"):
|
if self.filters.get("sales_invoice"):
|
||||||
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
conditions += " and `tabSales Invoice`.name = %(sales_invoice)s"
|
||||||
|
|
||||||
@@ -644,10 +719,12 @@ class GrossProfitGenerator(object):
|
|||||||
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
`tabSales Invoice Item`.name as "item_row", `tabSales Invoice`.is_return,
|
||||||
`tabSales Invoice Item`.cost_center
|
`tabSales Invoice Item`.cost_center
|
||||||
{sales_person_cols}
|
{sales_person_cols}
|
||||||
|
{payment_term_cols}
|
||||||
from
|
from
|
||||||
`tabSales Invoice` inner join `tabSales Invoice Item`
|
`tabSales Invoice` inner join `tabSales Invoice Item`
|
||||||
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
on `tabSales Invoice Item`.parent = `tabSales Invoice`.name
|
||||||
{sales_team_table}
|
{sales_team_table}
|
||||||
|
{payment_term_table}
|
||||||
where
|
where
|
||||||
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
`tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond}
|
||||||
order by
|
order by
|
||||||
@@ -655,6 +732,8 @@ class GrossProfitGenerator(object):
|
|||||||
conditions=conditions,
|
conditions=conditions,
|
||||||
sales_person_cols=sales_person_cols,
|
sales_person_cols=sales_person_cols,
|
||||||
sales_team_table=sales_team_table,
|
sales_team_table=sales_team_table,
|
||||||
|
payment_term_cols=payment_term_cols,
|
||||||
|
payment_term_table=payment_term_table,
|
||||||
match_cond=get_match_cond("Sales Invoice"),
|
match_cond=get_match_cond("Sales Invoice"),
|
||||||
),
|
),
|
||||||
self.filters,
|
self.filters,
|
||||||
|
|||||||
@@ -443,12 +443,6 @@ def get_grand_total(filters, doctype):
|
|||||||
] # nosec
|
] # 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(
|
def get_tax_accounts(
|
||||||
item_list,
|
item_list,
|
||||||
columns,
|
columns,
|
||||||
@@ -462,6 +456,7 @@ def get_tax_accounts(
|
|||||||
tax_columns = []
|
tax_columns = []
|
||||||
invoice_item_row = {}
|
invoice_item_row = {}
|
||||||
itemised_tax = {}
|
itemised_tax = {}
|
||||||
|
add_deduct_tax = "charge_type"
|
||||||
|
|
||||||
tax_amount_precision = (
|
tax_amount_precision = (
|
||||||
get_field_precision(
|
get_field_precision(
|
||||||
@@ -477,13 +472,13 @@ def get_tax_accounts(
|
|||||||
conditions = ""
|
conditions = ""
|
||||||
if doctype == "Purchase Invoice":
|
if doctype == "Purchase Invoice":
|
||||||
conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0"
|
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(
|
tax_details = frappe.db.sql(
|
||||||
"""
|
"""
|
||||||
select
|
select
|
||||||
name, parent, description, item_wise_tax_detail,
|
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`
|
from `tab%s`
|
||||||
where
|
where
|
||||||
parenttype = %s and docstatus = 1
|
parenttype = %s and docstatus = 1
|
||||||
@@ -491,12 +486,22 @@ def get_tax_accounts(
|
|||||||
and parent in (%s)
|
and parent in (%s)
|
||||||
%s
|
%s
|
||||||
order by description
|
order by description
|
||||||
"""
|
""".format(
|
||||||
|
add_deduct_tax=add_deduct_tax
|
||||||
|
)
|
||||||
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
% (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions),
|
||||||
tuple([doctype] + list(invoice_item_row)),
|
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)
|
description = handle_html(description)
|
||||||
if description not in tax_columns and tax_amount:
|
if description not in tax_columns and tax_amount:
|
||||||
# as description is text editor earlier and markup can break the column convention in reports
|
# 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:
|
if item_tax_amount:
|
||||||
tax_value = flt(item_tax_amount, tax_amount_precision)
|
tax_value = flt(item_tax_amount, tax_amount_precision)
|
||||||
tax_value = (
|
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(
|
itemised_tax.setdefault(d.name, {})[description] = frappe._dict(
|
||||||
|
|||||||
@@ -211,6 +211,7 @@ def set_gl_entries_by_account(
|
|||||||
{additional_conditions}
|
{additional_conditions}
|
||||||
and posting_date <= %(to_date)s
|
and posting_date <= %(to_date)s
|
||||||
and {based_on} is not null
|
and {based_on} is not null
|
||||||
|
and is_cancelled = 0
|
||||||
order by {based_on}, posting_date""".format(
|
order by {based_on}, posting_date""".format(
|
||||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
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):
|
def get_conditions(filters):
|
||||||
conditions = ""
|
conditions = ""
|
||||||
|
|
||||||
|
accounting_dimensions = get_accounting_dimensions(as_list=False) or []
|
||||||
|
accounting_dimensions_list = [d.fieldname for d in accounting_dimensions]
|
||||||
|
|
||||||
if filters.get("company"):
|
if filters.get("company"):
|
||||||
conditions += " and company=%(company)s"
|
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"
|
conditions += " and customer = %(customer)s"
|
||||||
|
|
||||||
if filters.get("from_date"):
|
if filters.get("from_date"):
|
||||||
@@ -359,32 +363,18 @@ def get_conditions(filters):
|
|||||||
if filters.get("owner"):
|
if filters.get("owner"):
|
||||||
conditions += " and owner = %(owner)s"
|
conditions += " and owner = %(owner)s"
|
||||||
|
|
||||||
if filters.get("mode_of_payment"):
|
def get_sales_invoice_item_field_condition(field, table="Sales Invoice Item") -> str:
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Payment`
|
if not filters.get(field) or field in accounting_dimensions_list:
|
||||||
where parent=`tabSales Invoice`.name
|
return ""
|
||||||
and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)"""
|
return f""" and exists(select name from `tab{table}`
|
||||||
|
where parent=`tabSales Invoice`.name
|
||||||
|
and ifnull(`tab{table}`.{field}, '') = %({field})s)"""
|
||||||
|
|
||||||
if filters.get("cost_center"):
|
conditions += get_sales_invoice_item_field_condition("mode_of_payments", "Sales Invoice Payment")
|
||||||
conditions += """ and exists(select name from `tabSales Invoice Item`
|
conditions += get_sales_invoice_item_field_condition("cost_center")
|
||||||
where parent=`tabSales Invoice`.name
|
conditions += get_sales_invoice_item_field_condition("warehouse")
|
||||||
and ifnull(`tabSales Invoice Item`.cost_center, '') = %(cost_center)s)"""
|
conditions += get_sales_invoice_item_field_condition("brand")
|
||||||
|
conditions += get_sales_invoice_item_field_condition("item_group")
|
||||||
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)
|
|
||||||
|
|
||||||
if accounting_dimensions:
|
if accounting_dimensions:
|
||||||
common_condition = """
|
common_condition = """
|
||||||
|
|||||||
@@ -160,14 +160,12 @@ def get_rootwise_opening_balances(filters, report_type):
|
|||||||
if filters.project:
|
if filters.project:
|
||||||
additional_conditions += " and project = %(project)s"
|
additional_conditions += " and project = %(project)s"
|
||||||
|
|
||||||
if filters.finance_book:
|
if filters.get("include_default_book_entries"):
|
||||||
fb_conditions = " AND finance_book = %(finance_book)s"
|
additional_conditions += (
|
||||||
if filters.include_default_book_entries:
|
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
||||||
fb_conditions = (
|
)
|
||||||
" AND (finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)"
|
else:
|
||||||
)
|
additional_conditions += " AND (finance_book in (%(finance_book)s, '') OR finance_book IS NULL)"
|
||||||
|
|
||||||
additional_conditions += fb_conditions
|
|
||||||
|
|
||||||
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
accounting_dimensions = get_accounting_dimensions(as_list=False)
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
|
|||||||
("Item-wise Sales Register", {}),
|
("Item-wise Sales Register", {}),
|
||||||
("Item-wise Purchase Register", {}),
|
("Item-wise Purchase Register", {}),
|
||||||
("Sales Register", {}),
|
("Sales Register", {}),
|
||||||
|
("Sales Register", {"item_group": "All Item Groups"}),
|
||||||
("Purchase Register", {}),
|
("Purchase Register", {}),
|
||||||
(
|
(
|
||||||
"Tax Detail",
|
"Tax Detail",
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ from typing import List, Tuple
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.defaults
|
import frappe.defaults
|
||||||
from frappe import _, throw
|
from frappe import _, qb, throw
|
||||||
from frappe.model.meta import get_field_precision
|
from frappe.model.meta import get_field_precision
|
||||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
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
|
# imported to enable erpnext.accounts.utils.get_account_currency
|
||||||
from erpnext.accounts.doctype.account.account import get_account_currency # noqa
|
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 import get_warehouse_account_map
|
||||||
from erpnext.stock.utils import get_stock_value_on
|
from erpnext.stock.utils import get_stock_value_on
|
||||||
|
|
||||||
@@ -1123,6 +1124,9 @@ def update_gl_entries_after(
|
|||||||
def repost_gle_for_stock_vouchers(
|
def repost_gle_for_stock_vouchers(
|
||||||
stock_vouchers, posting_date, company=None, warehouse_account=None
|
stock_vouchers, posting_date, company=None, warehouse_account=None
|
||||||
):
|
):
|
||||||
|
|
||||||
|
from erpnext.accounts.general_ledger import toggle_debit_credit_if_negative
|
||||||
|
|
||||||
if not stock_vouchers:
|
if not stock_vouchers:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1141,10 +1145,12 @@ def repost_gle_for_stock_vouchers(
|
|||||||
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
|
||||||
|
|
||||||
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
|
gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
|
||||||
for voucher_type, voucher_no in stock_vouchers:
|
for idx, (voucher_type, voucher_no) in enumerate(stock_vouchers):
|
||||||
existing_gle = gle.get((voucher_type, voucher_no), [])
|
existing_gle = gle.get((voucher_type, voucher_no), [])
|
||||||
voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
|
voucher_obj = frappe.get_doc(voucher_type, voucher_no)
|
||||||
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
|
# Some transactions post credit as negative debit, this is handled while posting GLE
|
||||||
|
# but while comparing we need to make sure it's flipped so comparisons are accurate
|
||||||
|
expected_gle = toggle_debit_credit_if_negative(voucher_obj.get_gl_entries(warehouse_account))
|
||||||
if expected_gle:
|
if expected_gle:
|
||||||
if not existing_gle or not compare_existing_and_expected_gle(
|
if not existing_gle or not compare_existing_and_expected_gle(
|
||||||
existing_gle, expected_gle, precision
|
existing_gle, expected_gle, precision
|
||||||
@@ -1154,6 +1160,11 @@ def repost_gle_for_stock_vouchers(
|
|||||||
else:
|
else:
|
||||||
_delete_gl_entries(voucher_type, voucher_no)
|
_delete_gl_entries(voucher_type, voucher_no)
|
||||||
|
|
||||||
|
if idx % 20 == 0:
|
||||||
|
# Commit every 20 documents to avoid losing progress
|
||||||
|
# and reducing memory usage
|
||||||
|
frappe.db.commit()
|
||||||
|
|
||||||
|
|
||||||
def sort_stock_vouchers_by_posting_date(
|
def sort_stock_vouchers_by_posting_date(
|
||||||
stock_vouchers: List[Tuple[str, str]]
|
stock_vouchers: List[Tuple[str, str]]
|
||||||
@@ -1345,3 +1356,102 @@ def check_and_delete_linked_reports(report):
|
|||||||
if icons:
|
if icons:
|
||||||
for icon in icons:
|
for icon in icons:
|
||||||
frappe.delete_doc("Desktop Icon", icon)
|
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()
|
||||||
|
|||||||
@@ -504,18 +504,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"dependencies": "GL Entry",
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 1,
|
|
||||||
"label": "DATEV Export",
|
|
||||||
"link_count": 0,
|
|
||||||
"link_to": "DATEV",
|
|
||||||
"link_type": "Report",
|
|
||||||
"onboard": 0,
|
|
||||||
"only_for": "Germany",
|
|
||||||
"type": "Link"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "GL Entry",
|
"dependencies": "GL Entry",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -1024,16 +1012,16 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "Cost Center",
|
"dependencies": "Cost Center",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
"label": "Cost Center Allocation",
|
"label": "Cost Center Allocation",
|
||||||
"link_count": 0,
|
"link_count": 0,
|
||||||
"link_to": "Cost Center Allocation",
|
"link_to": "Cost Center Allocation",
|
||||||
"link_type": "DocType",
|
"link_type": "DocType",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"dependencies": "Cost Center",
|
"dependencies": "Cost Center",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -1235,13 +1223,14 @@
|
|||||||
"type": "Link"
|
"type": "Link"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"modified": "2022-01-13 17:25:09.835345",
|
"modified": "2022-06-10 15:49:42.990860",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Accounting",
|
"name": "Accounting",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"parent_page": "",
|
"parent_page": "",
|
||||||
"public": 1,
|
"public": 1,
|
||||||
|
"quick_lists": [],
|
||||||
"restrict_to_domain": "",
|
"restrict_to_domain": "",
|
||||||
"roles": [],
|
"roles": [],
|
||||||
"sequence_id": 2.0,
|
"sequence_id": 2.0,
|
||||||
|
|||||||
@@ -148,7 +148,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-04-14 15:56:42.340223",
|
"modified": "2022-05-31 19:40:26.103909",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Buying Settings",
|
"name": "Buying Settings",
|
||||||
@@ -162,6 +162,16 @@
|
|||||||
"role": "System Manager",
|
"role": "System Manager",
|
||||||
"share": 1,
|
"share": 1,
|
||||||
"write": 1
|
"write": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"create": 1,
|
||||||
|
"email": 1,
|
||||||
|
"export": 1,
|
||||||
|
"print": 1,
|
||||||
|
"read": 1,
|
||||||
|
"role": "Purchase Manager",
|
||||||
|
"share": 1,
|
||||||
|
"write": 1
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ class BuyingSettings(Document):
|
|||||||
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
|
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
|
||||||
frappe.db.set_default(key, self.get(key, ""))
|
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(
|
set_by_naming_series(
|
||||||
"Supplier",
|
"Supplier",
|
||||||
|
|||||||
@@ -43,8 +43,6 @@ frappe.ui.form.on("Purchase Order", {
|
|||||||
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
erpnext.queries.setup_queries(frm, "Warehouse", function() {
|
||||||
return erpnext.queries.warehouse(frm.doc);
|
return erpnext.queries.warehouse(frm.doc);
|
||||||
});
|
});
|
||||||
|
|
||||||
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
apply_tds: function(frm) {
|
apply_tds: function(frm) {
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
update_linked_doc(self.doctype, self.name, self.inter_company_order_reference)
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.ignore_linked_doctypes = "Payment Ledger Entry"
|
||||||
super(PurchaseOrder, self).on_cancel()
|
super(PurchaseOrder, self).on_cancel()
|
||||||
|
|
||||||
if self.is_against_so():
|
if self.is_against_so():
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ class Supplier(TransactionBase):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
def validate_internal_supplier(self):
|
def validate_internal_supplier(self):
|
||||||
|
if not self.is_internal_supplier:
|
||||||
|
self.represents_company = ""
|
||||||
|
|
||||||
internal_supplier = frappe.db.get_value(
|
internal_supplier = frappe.db.get_value(
|
||||||
"Supplier",
|
"Supplier",
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ from erpnext.accounts.doctype.pricing_rule.utils import (
|
|||||||
from erpnext.accounts.party import (
|
from erpnext.accounts.party import (
|
||||||
get_party_account,
|
get_party_account,
|
||||||
get_party_account_currency,
|
get_party_account_currency,
|
||||||
|
get_party_gle_currency,
|
||||||
validate_party_frozen_disabled,
|
validate_party_frozen_disabled,
|
||||||
)
|
)
|
||||||
from erpnext.accounts.utils import get_account_currency, get_fiscal_years, validate_fiscal_year
|
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_party()
|
||||||
self.validate_currency()
|
self.validate_currency()
|
||||||
|
self.validate_party_account_currency()
|
||||||
|
|
||||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
if self.doctype in ["Purchase Invoice", "Sales Invoice"]:
|
||||||
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
pos_check_field = "is_pos" if self.doctype == "Sales Invoice" else "is_paid"
|
||||||
@@ -1130,11 +1132,10 @@ class AccountsController(TransactionBase):
|
|||||||
{
|
{
|
||||||
"account": item.discount_account,
|
"account": item.discount_account,
|
||||||
"against": supplier_or_customer,
|
"against": supplier_or_customer,
|
||||||
dr_or_cr: flt(discount_amount, item.precision("discount_amount")),
|
dr_or_cr: flt(
|
||||||
dr_or_cr
|
|
||||||
+ "_in_account_currency": flt(
|
|
||||||
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
||||||
),
|
),
|
||||||
|
dr_or_cr + "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
|
||||||
"cost_center": item.cost_center,
|
"cost_center": item.cost_center,
|
||||||
"project": item.project,
|
"project": item.project,
|
||||||
},
|
},
|
||||||
@@ -1149,11 +1150,11 @@ class AccountsController(TransactionBase):
|
|||||||
{
|
{
|
||||||
"account": income_or_expense_account,
|
"account": income_or_expense_account,
|
||||||
"against": supplier_or_customer,
|
"against": supplier_or_customer,
|
||||||
rev_dr_cr: flt(discount_amount, item.precision("discount_amount")),
|
rev_dr_cr: flt(
|
||||||
rev_dr_cr
|
|
||||||
+ "_in_account_currency": flt(
|
|
||||||
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
discount_amount * self.get("conversion_rate"), item.precision("discount_amount")
|
||||||
),
|
),
|
||||||
|
rev_dr_cr
|
||||||
|
+ "_in_account_currency": flt(discount_amount, item.precision("discount_amount")),
|
||||||
"cost_center": item.cost_center,
|
"cost_center": item.cost_center,
|
||||||
"project": item.project or self.project,
|
"project": item.project or self.project,
|
||||||
},
|
},
|
||||||
@@ -1448,6 +1449,27 @@ class AccountsController(TransactionBase):
|
|||||||
# at quotation / sales order level and we shouldn't stop someone
|
# at quotation / sales order level and we shouldn't stop someone
|
||||||
# from creating a sales invoice if sales order is already created
|
# 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):
|
def delink_advance_entries(self, linked_doc_name):
|
||||||
total_allocated_amount = 0
|
total_allocated_amount = 0
|
||||||
for adv in self.advances:
|
for adv in self.advances:
|
||||||
@@ -1844,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):
|
def get_taxes_and_charges(master_doctype, master_name):
|
||||||
if not master_name:
|
if not master_name:
|
||||||
return
|
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)
|
tax_master = frappe.get_doc(master_doctype, master_name)
|
||||||
|
|
||||||
@@ -1852,7 +1874,7 @@ def get_taxes_and_charges(master_doctype, master_name):
|
|||||||
for i, tax in enumerate(tax_master.get("taxes")):
|
for i, tax in enumerate(tax_master.get("taxes")):
|
||||||
tax = tax.as_dict()
|
tax = tax.as_dict()
|
||||||
|
|
||||||
for fieldname in default_fields:
|
for fieldname in default_fields + child_table_fields:
|
||||||
if fieldname in tax:
|
if fieldname in tax:
|
||||||
del tax[fieldname]
|
del tax[fieldname]
|
||||||
|
|
||||||
@@ -2639,7 +2661,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
|||||||
parent.update_reserved_qty_for_subcontract()
|
parent.update_reserved_qty_for_subcontract()
|
||||||
parent.create_raw_materials_supplied("supplied_items")
|
parent.create_raw_materials_supplied("supplied_items")
|
||||||
parent.save()
|
parent.save()
|
||||||
else:
|
else: # Sales Order
|
||||||
|
parent.validate_warehouse()
|
||||||
parent.update_reserved_qty()
|
parent.update_reserved_qty()
|
||||||
parent.update_project()
|
parent.update_project()
|
||||||
parent.update_prevdoc_status("submit")
|
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]
|
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 frappe.model.mapper import get_mapped_doc
|
||||||
|
|
||||||
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
|
||||||
|
|||||||
@@ -34,9 +34,7 @@ class WebsiteItem(WebsiteGenerator):
|
|||||||
|
|
||||||
def autoname(self):
|
def autoname(self):
|
||||||
# use naming series to accomodate items with same name (different item code)
|
# use naming series to accomodate items with same name (different item code)
|
||||||
from frappe.model.naming import make_autoname
|
from frappe.model.naming import get_default_naming_series, make_autoname
|
||||||
|
|
||||||
from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
|
|
||||||
|
|
||||||
naming_series = get_default_naming_series("Website Item")
|
naming_series = get_default_naming_series("Website Item")
|
||||||
if not self.name and naming_series:
|
if not self.name and naming_series:
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ def is_search_module_loaded():
|
|||||||
out = cache.execute_command("MODULE LIST")
|
out = cache.execute_command("MODULE LIST")
|
||||||
|
|
||||||
parsed_output = " ".join(
|
parsed_output = " ".join(
|
||||||
(" ".join([s.decode() for s in o if not isinstance(s, int)]) for o in out)
|
(" ".join([frappe.as_unicode(s) for s in o if not isinstance(s, int)]) for o in out)
|
||||||
)
|
)
|
||||||
return "search" in parsed_output
|
return "search" in parsed_output
|
||||||
except Exception:
|
except Exception:
|
||||||
|
|||||||
@@ -1,44 +0,0 @@
|
|||||||
import frappe
|
|
||||||
from frappe.data_migration.doctype.data_migration_connector.connectors.base import BaseConnection
|
|
||||||
from github import Github
|
|
||||||
|
|
||||||
class GithubConnection(BaseConnection):
|
|
||||||
def __init__(self, connector):
|
|
||||||
self.connector = connector
|
|
||||||
|
|
||||||
try:
|
|
||||||
password = self.get_password()
|
|
||||||
except frappe.AuthenticationError:
|
|
||||||
password = None
|
|
||||||
|
|
||||||
if self.connector.username and password:
|
|
||||||
self.connection = Github(self.connector.username, self.get_password())
|
|
||||||
else:
|
|
||||||
self.connection = Github()
|
|
||||||
|
|
||||||
self.name_field = 'id'
|
|
||||||
|
|
||||||
def insert(self, doctype, doc):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def update(self, doctype, doc, migration_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def delete(self, doctype, migration_id):
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get(self, remote_objectname, fields=None, filters=None, start=0, page_length=10):
|
|
||||||
repo = filters.get('repo')
|
|
||||||
|
|
||||||
if remote_objectname == 'Milestone':
|
|
||||||
return self.get_milestones(repo, start, page_length)
|
|
||||||
if remote_objectname == 'Issue':
|
|
||||||
return self.get_issues(repo, start, page_length)
|
|
||||||
|
|
||||||
def get_milestones(self, repo, start=0, page_length=10):
|
|
||||||
_repo = self.connection.get_repo(repo)
|
|
||||||
return list(_repo.get_milestones()[start:start+page_length])
|
|
||||||
|
|
||||||
def get_issues(self, repo, start=0, page_length=10):
|
|
||||||
_repo = self.connection.get_repo(repo)
|
|
||||||
return list(_repo.get_issues()[start:start+page_length])
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import frappe
|
|
||||||
|
|
||||||
|
|
||||||
def pre_process(issue):
|
|
||||||
|
|
||||||
project = frappe.db.get_value("Project", filters={"project_name": issue.milestone})
|
|
||||||
return {
|
|
||||||
"title": issue.title,
|
|
||||||
"body": frappe.utils.md_to_html(issue.body or ""),
|
|
||||||
"state": issue.state.title(),
|
|
||||||
"project": project or "",
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"condition": "{\"repo\":\"frappe/erpnext\"}",
|
|
||||||
"creation": "2017-10-16 16:03:32.772191",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Data Migration Mapping",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "subject",
|
|
||||||
"remote_fieldname": "title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "description",
|
|
||||||
"remote_fieldname": "body"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "status",
|
|
||||||
"remote_fieldname": "state"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idx": 0,
|
|
||||||
"local_doctype": "Task",
|
|
||||||
"local_primary_key": "name",
|
|
||||||
"mapping_name": "Issue to Task",
|
|
||||||
"mapping_type": "Pull",
|
|
||||||
"migration_id_field": "github_sync_id",
|
|
||||||
"modified": "2017-10-20 11:48:54.575993",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"name": "Issue to Task",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"page_length": 10,
|
|
||||||
"remote_objectname": "Issue",
|
|
||||||
"remote_primary_key": "id"
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
def pre_process(milestone):
|
|
||||||
return {
|
|
||||||
"title": milestone.title,
|
|
||||||
"description": milestone.description,
|
|
||||||
"state": milestone.state.title(),
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
{
|
|
||||||
"condition": "{\"repo\": \"frappe/erpnext\"}",
|
|
||||||
"creation": "2017-10-13 11:16:49.664925",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Data Migration Mapping",
|
|
||||||
"fields": [
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "project_name",
|
|
||||||
"remote_fieldname": "title"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "notes",
|
|
||||||
"remote_fieldname": "description"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"is_child_table": 0,
|
|
||||||
"local_fieldname": "status",
|
|
||||||
"remote_fieldname": "state"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"idx": 0,
|
|
||||||
"local_doctype": "Project",
|
|
||||||
"local_primary_key": "project_name",
|
|
||||||
"mapping_name": "Milestone to Project",
|
|
||||||
"mapping_type": "Pull",
|
|
||||||
"migration_id_field": "github_sync_id",
|
|
||||||
"modified": "2017-10-20 11:48:54.552305",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"name": "Milestone to Project",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"page_length": 10,
|
|
||||||
"remote_objectname": "Milestone",
|
|
||||||
"remote_primary_key": "id"
|
|
||||||
}
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
{
|
|
||||||
"creation": "2017-10-13 11:16:53.600026",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Data Migration Plan",
|
|
||||||
"idx": 0,
|
|
||||||
"mappings": [
|
|
||||||
{
|
|
||||||
"enabled": 1,
|
|
||||||
"mapping": "Milestone to Project"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enabled": 1,
|
|
||||||
"mapping": "Issue to Task"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"modified": "2017-10-20 11:48:54.496123",
|
|
||||||
"modified_by": "Administrator",
|
|
||||||
"module": "ERPNext Integrations",
|
|
||||||
"name": "GitHub Sync",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"plan_name": "GitHub Sync"
|
|
||||||
}
|
|
||||||
@@ -392,9 +392,12 @@ after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
|
|||||||
|
|
||||||
scheduler_events = {
|
scheduler_events = {
|
||||||
"cron": {
|
"cron": {
|
||||||
|
"0/5 * * * *": [
|
||||||
|
"erpnext.manufacturing.doctype.bom_update_log.bom_update_log.resume_bom_cost_update_jobs",
|
||||||
|
],
|
||||||
"0/30 * * * *": [
|
"0/30 * * * *": [
|
||||||
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
"erpnext.utilities.doctype.video.video.update_youtube_data",
|
||||||
]
|
],
|
||||||
},
|
},
|
||||||
"all": [
|
"all": [
|
||||||
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
"erpnext.projects.doctype.project.project.project_status_update_reminder",
|
||||||
@@ -487,6 +490,7 @@ communication_doctypes = ["Customer", "Supplier"]
|
|||||||
|
|
||||||
accounting_dimension_doctypes = [
|
accounting_dimension_doctypes = [
|
||||||
"GL Entry",
|
"GL Entry",
|
||||||
|
"Payment Ledger Entry",
|
||||||
"Sales Invoice",
|
"Sales Invoice",
|
||||||
"Purchase Invoice",
|
"Purchase Invoice",
|
||||||
"Payment Entry",
|
"Payment Entry",
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ class Attendance(Document):
|
|||||||
self.validate_employee_status()
|
self.validate_employee_status()
|
||||||
self.check_leave_record()
|
self.check_leave_record()
|
||||||
|
|
||||||
|
def on_cancel(self):
|
||||||
|
self.unlink_attendance_from_checkins()
|
||||||
|
|
||||||
def validate_attendance_date(self):
|
def validate_attendance_date(self):
|
||||||
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
date_of_joining = frappe.db.get_value("Employee", self.employee, "date_of_joining")
|
||||||
|
|
||||||
@@ -127,6 +130,33 @@ class Attendance(Document):
|
|||||||
if not emp:
|
if not emp:
|
||||||
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
frappe.throw(_("Employee {0} is not active or does not exist").format(self.employee))
|
||||||
|
|
||||||
|
def unlink_attendance_from_checkins(self):
|
||||||
|
EmployeeCheckin = frappe.qb.DocType("Employee Checkin")
|
||||||
|
linked_logs = (
|
||||||
|
frappe.qb.from_(EmployeeCheckin)
|
||||||
|
.select(EmployeeCheckin.name)
|
||||||
|
.where(EmployeeCheckin.attendance == self.name)
|
||||||
|
.for_update()
|
||||||
|
.run(as_dict=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
if linked_logs:
|
||||||
|
(
|
||||||
|
frappe.qb.update(EmployeeCheckin)
|
||||||
|
.set("attendance", "")
|
||||||
|
.where(EmployeeCheckin.attendance == self.name)
|
||||||
|
).run()
|
||||||
|
|
||||||
|
frappe.msgprint(
|
||||||
|
msg=_("Unlinked Attendance record from Employee Checkins: {}").format(
|
||||||
|
", ".join(get_link_to_form("Employee Checkin", log.name) for log in linked_logs)
|
||||||
|
),
|
||||||
|
title=_("Unlinked logs"),
|
||||||
|
indicator="blue",
|
||||||
|
is_minimizable=True,
|
||||||
|
wide=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
|
def get_duplicate_attendance_record(employee, attendance_date, shift, name=None):
|
||||||
attendance = frappe.qb.DocType("Attendance")
|
attendance = frappe.qb.DocType("Attendance")
|
||||||
|
|||||||
@@ -3,7 +3,15 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.tests.utils import FrappeTestCase
|
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 (
|
from erpnext.hr.doctype.attendance.attendance import (
|
||||||
DuplicateAttendanceError,
|
DuplicateAttendanceError,
|
||||||
@@ -138,69 +146,70 @@ class TestAttendance(FrappeTestCase):
|
|||||||
self.assertEqual(attendance, fetch_attendance)
|
self.assertEqual(attendance, fetch_attendance)
|
||||||
|
|
||||||
def test_unmarked_days(self):
|
def test_unmarked_days(self):
|
||||||
now = now_datetime()
|
first_sunday = get_first_sunday(
|
||||||
previous_month = now.month - 1
|
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
)
|
||||||
|
attendance_date = add_days(first_sunday, 1)
|
||||||
|
|
||||||
employee = make_employee(
|
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)
|
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, attendance_date, "Present")
|
||||||
mark_attendance(employee, first_day, "Present")
|
month_name = get_month_name(attendance_date)
|
||||||
month_name = get_month_name(first_day)
|
|
||||||
|
|
||||||
unmarked_days = get_unmarked_days(employee, month_name)
|
unmarked_days = get_unmarked_days(employee, month_name)
|
||||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
self.assertNotIn(first_day, unmarked_days)
|
self.assertNotIn(attendance_date, unmarked_days)
|
||||||
# attendance unmarked
|
# 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
|
# holiday considered in unmarked days
|
||||||
self.assertIn(first_sunday, unmarked_days)
|
self.assertIn(first_sunday, unmarked_days)
|
||||||
|
|
||||||
def test_unmarked_days_excluding_holidays(self):
|
def test_unmarked_days_excluding_holidays(self):
|
||||||
now = now_datetime()
|
first_sunday = get_first_sunday(
|
||||||
previous_month = now.month - 1
|
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
)
|
||||||
|
attendance_date = add_days(first_sunday, 1)
|
||||||
|
|
||||||
employee = make_employee(
|
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)
|
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, attendance_date, "Present")
|
||||||
mark_attendance(employee, first_day, "Present")
|
month_name = get_month_name(attendance_date)
|
||||||
month_name = get_month_name(first_day)
|
|
||||||
|
|
||||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|
||||||
# attendance already marked for the day
|
# attendance already marked for the day
|
||||||
self.assertNotIn(first_day, unmarked_days)
|
self.assertNotIn(attendance_date, unmarked_days)
|
||||||
# attendance unmarked
|
# 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
|
# holidays not considered in unmarked days
|
||||||
self.assertNotIn(first_sunday, unmarked_days)
|
self.assertNotIn(first_sunday, unmarked_days)
|
||||||
|
|
||||||
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||||
now = now_datetime()
|
first_sunday = get_first_sunday(
|
||||||
previous_month = now.month - 1
|
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
)
|
||||||
|
date = add_days(first_sunday, 1)
|
||||||
|
|
||||||
doj = add_days(first_day, 1)
|
doj = add_days(date, 1)
|
||||||
relieving_date = add_days(first_day, 5)
|
relieving_date = add_days(date, 5)
|
||||||
employee = make_employee(
|
employee = make_employee(
|
||||||
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
|
"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)
|
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")
|
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 = get_unmarked_days(employee, month_name)
|
||||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||||
|
|||||||
@@ -827,7 +827,7 @@
|
|||||||
"idx": 24,
|
"idx": 24,
|
||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-04-22 16:21:55.811983",
|
"modified": "2022-06-10 01:29:32.952091",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "Employee",
|
"name": "Employee",
|
||||||
@@ -872,7 +872,6 @@
|
|||||||
],
|
],
|
||||||
"search_fields": "employee_name",
|
"search_fields": "employee_name",
|
||||||
"show_name_in_global_search": 1,
|
"show_name_in_global_search": 1,
|
||||||
"show_title_field_in_link": 1,
|
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
|
|||||||
@@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
|||||||
persons_name = anniversary_person
|
persons_name = anniversary_person
|
||||||
# Number of years completed at the company
|
# Number of years completed at the company
|
||||||
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
|
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:
|
else:
|
||||||
person_names_with_years = []
|
person_names_with_years = []
|
||||||
names = []
|
names = []
|
||||||
@@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
|||||||
names.append(person_text)
|
names.append(person_text)
|
||||||
# Number of years completed at the company
|
# Number of years completed at the company
|
||||||
completed_years = getdate().year - person["date_of_joining"].year
|
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)
|
person_names_with_years.append(person_text)
|
||||||
|
|
||||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
# 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
|
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):
|
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||||
frappe.sendmail(
|
frappe.sendmail(
|
||||||
recipients=recipients,
|
recipients=recipients,
|
||||||
|
|||||||
@@ -76,6 +76,17 @@ class TestEmployeeCheckin(FrappeTestCase):
|
|||||||
)
|
)
|
||||||
self.assertEqual(attendance_count, 1)
|
self.assertEqual(attendance_count, 1)
|
||||||
|
|
||||||
|
def test_unlink_attendance_on_cancellation(self):
|
||||||
|
employee = make_employee("test_mark_attendance_and_link_log@example.com")
|
||||||
|
logs = make_n_checkins(employee, 3)
|
||||||
|
|
||||||
|
frappe.db.delete("Attendance", {"employee": employee})
|
||||||
|
attendance = mark_attendance_and_link_log(logs, "Present", nowdate(), 8.2)
|
||||||
|
attendance.cancel()
|
||||||
|
|
||||||
|
linked_logs = frappe.db.get_all("Employee Checkin", {"attendance": attendance.name})
|
||||||
|
self.assertEquals(len(linked_logs), 0)
|
||||||
|
|
||||||
def test_calculate_working_hours(self):
|
def test_calculate_working_hours(self):
|
||||||
check_in_out_type = [
|
check_in_out_type = [
|
||||||
"Alternating entries as IN and OUT during the same shift",
|
"Alternating entries as IN and OUT during the same shift",
|
||||||
|
|||||||
@@ -105,7 +105,7 @@ class ExpenseClaim(AccountsController):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_task_and_project()
|
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:
|
if self.payable_account:
|
||||||
self.make_gl_entries(cancel=True)
|
self.make_gl_entries(cancel=True)
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ class HRSettings(Document):
|
|||||||
PROCEED_WITH_FREQUENCY_CHANGE = False
|
PROCEED_WITH_FREQUENCY_CHANGE = False
|
||||||
|
|
||||||
def set_naming_series(self):
|
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(
|
set_by_naming_series(
|
||||||
"Employee",
|
"Employee",
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from frappe.utils import get_link_to_form
|
||||||
from frappe.website.website_generator import WebsiteGenerator
|
from frappe.website.website_generator import WebsiteGenerator
|
||||||
|
|
||||||
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
|
from erpnext.hr.doctype.staffing_plan.staffing_plan import (
|
||||||
@@ -33,26 +34,32 @@ class JobOpening(WebsiteGenerator):
|
|||||||
self.staffing_plan = staffing_plan[0].name
|
self.staffing_plan = staffing_plan[0].name
|
||||||
self.planned_vacancies = staffing_plan[0].vacancies
|
self.planned_vacancies = staffing_plan[0].vacancies
|
||||||
elif not self.planned_vacancies:
|
elif not self.planned_vacancies:
|
||||||
planned_vacancies = frappe.db.sql(
|
self.planned_vacancies = frappe.db.get_value(
|
||||||
"""
|
"Staffing Plan Detail",
|
||||||
select vacancies from `tabStaffing Plan Detail`
|
{"parent": self.staffing_plan, "designation": self.designation},
|
||||||
where parent=%s and designation=%s""",
|
"vacancies",
|
||||||
(self.staffing_plan, self.designation),
|
|
||||||
)
|
)
|
||||||
self.planned_vacancies = planned_vacancies[0][0] if planned_vacancies else None
|
|
||||||
|
|
||||||
if self.staffing_plan and self.planned_vacancies:
|
if self.staffing_plan and self.planned_vacancies:
|
||||||
staffing_plan_company = frappe.db.get_value("Staffing Plan", self.staffing_plan, "company")
|
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"]
|
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(
|
frappe.throw(
|
||||||
_(
|
_(
|
||||||
"Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}"
|
"Job Openings for the designation {0} are already open or the hiring is complete as per the Staffing Plan {1}"
|
||||||
).format(self.designation, self.staffing_plan)
|
).format(
|
||||||
|
frappe.bold(self.designation), get_link_to_form("Staffing Plan", self.staffing_plan)
|
||||||
|
),
|
||||||
|
title=_("Vacancies fulfilled"),
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_context(self, context):
|
def get_context(self, context):
|
||||||
|
|||||||
@@ -3,8 +3,77 @@
|
|||||||
|
|
||||||
import unittest
|
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):
|
class TestJobOpening(FrappeTestCase):
|
||||||
pass
|
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,
|
date: frm.doc.from_date,
|
||||||
to_date: frm.doc.to_date,
|
to_date: frm.doc.to_date,
|
||||||
leave_type: frm.doc.leave_type,
|
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) {
|
callback: function (r) {
|
||||||
if (!r.exc && r.message) {
|
if (!r.exc && r.message) {
|
||||||
|
|||||||
@@ -88,7 +88,7 @@ class LeaveApplication(Document):
|
|||||||
share_doc_with_approver(self, self.leave_approver)
|
share_doc_with_approver(self, self.leave_approver)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
if self.status == "Open":
|
if self.status in ["Open", "Cancelled"]:
|
||||||
frappe.throw(
|
frappe.throw(
|
||||||
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
_("Only Leave Applications with status 'Approved' and 'Rejected' can be submitted")
|
||||||
)
|
)
|
||||||
@@ -757,22 +757,6 @@ def get_leave_details(employee, date):
|
|||||||
leave_allocation = {}
|
leave_allocation = {}
|
||||||
for d in allocation_records:
|
for d in allocation_records:
|
||||||
allocation = allocation_records.get(d, frappe._dict())
|
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(
|
remaining_leaves = get_leave_balance_on(
|
||||||
employee, d, date, to_date=allocation.to_date, consider_all_leaves_in_the_allocation_period=True
|
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(
|
leaves_pending = get_leaves_pending_approval_for_period(
|
||||||
employee, d, allocation.from_date, end_date
|
employee, d, allocation.from_date, end_date
|
||||||
)
|
)
|
||||||
|
expired_leaves = allocation.total_leaves_allocated - (remaining_leaves + leaves_taken)
|
||||||
|
|
||||||
leave_allocation[d] = {
|
leave_allocation[d] = {
|
||||||
"total_leaves": total_allocated_leaves,
|
"total_leaves": allocation.total_leaves_allocated,
|
||||||
"expired_leaves": total_allocated_leaves - (remaining_leaves + leaves_taken),
|
"expired_leaves": expired_leaves if expired_leaves > 0 else 0,
|
||||||
"leaves_taken": leaves_taken,
|
"leaves_taken": leaves_taken,
|
||||||
"leaves_pending_approval": leaves_pending,
|
"leaves_pending_approval": leaves_pending,
|
||||||
"remaining_leaves": remaining_leaves,
|
"remaining_leaves": remaining_leaves,
|
||||||
@@ -830,7 +815,7 @@ def get_leave_balance_on(
|
|||||||
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
allocation_records = get_leave_allocation_records(employee, date, leave_type)
|
||||||
allocation = allocation_records.get(leave_type, frappe._dict())
|
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)
|
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)
|
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
|
WHERE
|
||||||
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
from_date <= %(end)s AND to_date >= %(start)s <= to_date
|
||||||
AND docstatus < 2
|
AND docstatus < 2
|
||||||
AND status != 'Rejected'
|
AND status in ('Approved', 'Open')
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if conditions:
|
if conditions:
|
||||||
@@ -1201,24 +1186,33 @@ def get_mandatory_approval(doctype):
|
|||||||
|
|
||||||
|
|
||||||
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
def get_approved_leaves_for_period(employee, leave_type, from_date, to_date):
|
||||||
query = """
|
LeaveApplication = frappe.qb.DocType("Leave Application")
|
||||||
select employee, leave_type, from_date, to_date, total_leave_days
|
query = (
|
||||||
from `tabLeave Application`
|
frappe.qb.from_(LeaveApplication)
|
||||||
where employee=%(employee)s
|
.select(
|
||||||
and docstatus=1
|
LeaveApplication.employee,
|
||||||
and (from_date between %(from_date)s and %(to_date)s
|
LeaveApplication.leave_type,
|
||||||
or to_date between %(from_date)s and %(to_date)s
|
LeaveApplication.from_date,
|
||||||
or (from_date < %(from_date)s and to_date > %(to_date)s))
|
LeaveApplication.to_date,
|
||||||
"""
|
LeaveApplication.total_leave_days,
|
||||||
if leave_type:
|
)
|
||||||
query += "and leave_type=%(leave_type)s"
|
.where(
|
||||||
|
(LeaveApplication.employee == employee)
|
||||||
leave_applications = frappe.db.sql(
|
& (LeaveApplication.docstatus == 1)
|
||||||
query,
|
& (LeaveApplication.status == "Approved")
|
||||||
{"from_date": from_date, "to_date": to_date, "employee": employee, "leave_type": leave_type},
|
& (
|
||||||
as_dict=1,
|
(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
|
leave_days = 0
|
||||||
for leave_app in leave_applications:
|
for leave_app in leave_applications:
|
||||||
if leave_app.from_date >= getdate(from_date) and leave_app.to_date <= getdate(to_date):
|
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"],
|
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
|
||||||
has_indicator_for_draft: 1,
|
has_indicator_for_draft: 1,
|
||||||
get_indicator: function (doc) {
|
get_indicator: function (doc) {
|
||||||
if (doc.status === "Approved") {
|
let status_color = {
|
||||||
return [__("Approved"), "green", "status,=,Approved"];
|
"Approved": "green",
|
||||||
} else if (doc.status === "Rejected") {
|
"Rejected": "red",
|
||||||
return [__("Rejected"), "red", "status,=,Rejected"];
|
"Open": "orange",
|
||||||
} else {
|
"Cancelled": "red",
|
||||||
return [__("Open"), "red", "status,=,Open"];
|
"Submitted": "blue"
|
||||||
}
|
};
|
||||||
|
return [__(doc.status), status_color[doc.status], "status,=," + doc.status];
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,7 +76,14 @@ _test_records = [
|
|||||||
|
|
||||||
class TestLeaveApplication(unittest.TestCase):
|
class TestLeaveApplication(unittest.TestCase):
|
||||||
def setUp(self):
|
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.db.delete(dt)
|
||||||
|
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
@@ -702,59 +709,24 @@ class TestLeaveApplication(unittest.TestCase):
|
|||||||
self.assertEqual(details.leave_balance, 30)
|
self.assertEqual(details.leave_balance, 30)
|
||||||
|
|
||||||
def test_earned_leaves_creation(self):
|
def test_earned_leaves_creation(self):
|
||||||
|
from erpnext.hr.utils import allocate_earned_leaves
|
||||||
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`""")
|
|
||||||
|
|
||||||
leave_period = get_leave_period()
|
leave_period = get_leave_period()
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
leave_type = "Test Earned Leave Type"
|
leave_type = "Test Earned Leave Type"
|
||||||
frappe.delete_doc_if_exists("Leave Type", "Test Earned Leave Type", force=1)
|
make_policy_assignment(employee, leave_type, leave_period)
|
||||||
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(
|
for i in range(0, 14):
|
||||||
{
|
|
||||||
"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:
|
|
||||||
allocate_earned_leaves()
|
allocate_earned_leaves()
|
||||||
i += 1
|
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6)
|
||||||
|
|
||||||
# validate earned leaves creation without maximum leaves
|
# validate earned leaves creation without maximum leaves
|
||||||
frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0)
|
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()
|
allocate_earned_leaves()
|
||||||
i += 1
|
|
||||||
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9)
|
||||||
|
|
||||||
# test to not consider current leave in leave balance while submitting
|
# 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["leaves_pending_approval"], 1)
|
||||||
self.assertEqual(leave_allocation["remaining_leaves"], 26)
|
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")
|
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||||
def test_get_leave_allocation_records(self):
|
def test_get_leave_allocation_records(self):
|
||||||
employee = get_employee()
|
employee = get_employee()
|
||||||
@@ -1100,3 +1120,36 @@ def get_first_sunday(holiday_list, for_date=None):
|
|||||||
)[0][0]
|
)[0][0]
|
||||||
|
|
||||||
return first_sunday
|
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.model.document import Document
|
||||||
from frappe.utils import getdate, nowdate
|
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.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.hr.utils import set_employee_name, validate_active_employee
|
||||||
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
|
from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import (
|
||||||
@@ -107,7 +107,10 @@ class LeaveEncashment(Document):
|
|||||||
self.leave_balance = (
|
self.leave_balance = (
|
||||||
allocation.total_leaves_allocated
|
allocation.total_leaves_allocated
|
||||||
- allocation.carry_forwarded_leaves_count
|
- 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(
|
encashable_days = self.leave_balance - frappe.db.get_value(
|
||||||
@@ -126,14 +129,25 @@ class LeaveEncashment(Document):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def get_leave_allocation(self):
|
def get_leave_allocation(self):
|
||||||
leave_allocation = frappe.db.sql(
|
date = self.encashment_date or getdate()
|
||||||
"""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}'
|
LeaveAllocation = frappe.qb.DocType("Leave Allocation")
|
||||||
and employee= '{2}'""".format(
|
leave_allocation = (
|
||||||
self.encashment_date or getdate(nowdate()), self.leave_type, self.employee
|
frappe.qb.from_(LeaveAllocation)
|
||||||
),
|
.select(
|
||||||
as_dict=1,
|
LeaveAllocation.name,
|
||||||
) # nosec
|
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
|
return leave_allocation[0] if leave_allocation else None
|
||||||
|
|
||||||
|
|||||||
@@ -4,26 +4,42 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
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.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_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.test_leave_policy import create_leave_policy
|
||||||
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import (
|
||||||
create_assignment_for_multiple_employees,
|
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
|
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):
|
def setUp(self):
|
||||||
frappe.db.sql("""delete from `tabLeave Period`""")
|
frappe.db.delete("Leave Period")
|
||||||
frappe.db.sql("""delete from `tabLeave Policy Assignment`""")
|
frappe.db.delete("Leave Policy Assignment")
|
||||||
frappe.db.sql("""delete from `tabLeave Allocation`""")
|
frappe.db.delete("Leave Allocation")
|
||||||
frappe.db.sql("""delete from `tabLeave Ledger Entry`""")
|
frappe.db.delete("Leave Ledger Entry")
|
||||||
frappe.db.sql("""delete from `tabAdditional Salary`""")
|
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
|
# create the leave policy
|
||||||
leave_policy = create_leave_policy(
|
leave_policy = create_leave_policy(
|
||||||
@@ -32,9 +48,9 @@ class TestLeaveEncashment(unittest.TestCase):
|
|||||||
leave_policy.submit()
|
leave_policy.submit()
|
||||||
|
|
||||||
# create employee, salary structure and assignment
|
# 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 = {
|
data = {
|
||||||
"assignment_based_on": "Leave Period",
|
"assignment_based_on": "Leave Period",
|
||||||
@@ -53,27 +69,15 @@ class TestLeaveEncashment(unittest.TestCase):
|
|||||||
other_details={"leave_encashment_amount_per_day": 50},
|
other_details={"leave_encashment_amount_per_day": 50},
|
||||||
)
|
)
|
||||||
|
|
||||||
def tearDown(self):
|
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||||
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)
|
|
||||||
|
|
||||||
def test_leave_balance_value_and_amount(self):
|
def test_leave_balance_value_and_amount(self):
|
||||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
|
||||||
leave_encashment = frappe.get_doc(
|
leave_encashment = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
doctype="Leave Encashment",
|
doctype="Leave Encashment",
|
||||||
employee=self.employee,
|
employee=self.employee,
|
||||||
leave_type="_Test Leave Type Encashment",
|
leave_type="_Test Leave Type Encashment",
|
||||||
leave_period=self.leave_period.name,
|
leave_period=self.leave_period.name,
|
||||||
payroll_date=today(),
|
encashment_date=self.leave_period.to_date,
|
||||||
currency="INR",
|
currency="INR",
|
||||||
)
|
)
|
||||||
).insert()
|
).insert()
|
||||||
@@ -88,15 +92,46 @@ class TestLeaveEncashment(unittest.TestCase):
|
|||||||
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
add_sal = frappe.get_all("Additional Salary", filters={"ref_docname": leave_encashment.name})[0]
|
||||||
self.assertTrue(add_sal)
|
self.assertTrue(add_sal)
|
||||||
|
|
||||||
def test_creation_of_leave_ledger_entry_on_submit(self):
|
@set_holiday_list("_Test Leave Encashment", "_Test Company")
|
||||||
frappe.db.sql("""delete from `tabLeave Encashment`""")
|
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(
|
leave_encashment = frappe.get_doc(
|
||||||
dict(
|
dict(
|
||||||
doctype="Leave Encashment",
|
doctype="Leave Encashment",
|
||||||
employee=self.employee,
|
employee=self.employee,
|
||||||
leave_type="_Test Leave Type Encashment",
|
leave_type="_Test Leave Type Encashment",
|
||||||
leave_period=self.leave_period.name,
|
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",
|
currency="INR",
|
||||||
)
|
)
|
||||||
).insert()
|
).insert()
|
||||||
|
|||||||
@@ -172,27 +172,24 @@ class StaffingPlan(Document):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def get_designation_counts(designation, company):
|
def get_designation_counts(designation, company, job_opening=None):
|
||||||
if not designation:
|
if not designation:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
employee_counts = {}
|
|
||||||
company_set = get_descendants_of("Company", company)
|
company_set = get_descendants_of("Company", company)
|
||||||
company_set.append(company)
|
company_set.append(company)
|
||||||
|
|
||||||
employee_counts["employee_count"] = frappe.db.get_value(
|
employee_count = frappe.db.count(
|
||||||
"Employee",
|
"Employee", {"designation": designation, "status": "Active", "company": ("in", company_set)}
|
||||||
filters={"designation": designation, "status": "Active", "company": ("in", company_set)},
|
|
||||||
fieldname=["count(name)"],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
employee_counts["job_openings"] = frappe.db.get_value(
|
filters = {"designation": designation, "status": "Open", "company": ("in", company_set)}
|
||||||
"Job Opening",
|
if job_opening:
|
||||||
filters={"designation": designation, "status": "Open", "company": ("in", company_set)},
|
filters["name"] = ("!=", job_opening)
|
||||||
fieldname=["count(name)"],
|
|
||||||
)
|
|
||||||
|
|
||||||
return employee_counts
|
job_openings = frappe.db.count("Job Opening", filters)
|
||||||
|
|
||||||
|
return {"employee_count": employee_count, "job_openings": job_openings}
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
|
|||||||
@@ -85,13 +85,16 @@ def _set_up():
|
|||||||
make_company()
|
make_company()
|
||||||
|
|
||||||
|
|
||||||
def make_company():
|
def make_company(name=None, abbr=None):
|
||||||
if frappe.db.exists("Company", "_Test Company 10"):
|
if not name:
|
||||||
|
name = "_Test Company 10"
|
||||||
|
|
||||||
|
if frappe.db.exists("Company", name):
|
||||||
return
|
return
|
||||||
|
|
||||||
company = frappe.new_doc("Company")
|
company = frappe.new_doc("Company")
|
||||||
company.company_name = "_Test Company 10"
|
company.company_name = name
|
||||||
company.abbr = "_TC10"
|
company.abbr = abbr or "_TC10"
|
||||||
company.parent_company = "_Test Company 3"
|
company.parent_company = "_Test Company 3"
|
||||||
company.default_currency = "INR"
|
company.default_currency = "INR"
|
||||||
company.country = "Pakistan"
|
company.country = "Pakistan"
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ def update_employee_work_history(employee, details, date=None, cancel=False):
|
|||||||
new_data = getdate(new_data)
|
new_data = getdate(new_data)
|
||||||
elif fieldtype == "Datetime" and new_data:
|
elif fieldtype == "Datetime" and new_data:
|
||||||
new_data = get_datetime(new_data)
|
new_data = get_datetime(new_data)
|
||||||
|
elif fieldtype in ["Currency", "Float"] and new_data:
|
||||||
|
new_data = flt(new_data)
|
||||||
setattr(employee, item.fieldname, new_data)
|
setattr(employee, item.fieldname, new_data)
|
||||||
if item.fieldname in ["department", "designation", "branch"]:
|
if item.fieldname in ["department", "designation", "branch"]:
|
||||||
internal_work_history[item.fieldname] = item.new
|
internal_work_history[item.fieldname] = item.new
|
||||||
@@ -439,20 +441,18 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
def get_salary_assignment(employee, date):
|
def get_salary_assignments(employee, payroll_period):
|
||||||
assignment = frappe.db.sql(
|
start_date, end_date = frappe.db.get_value(
|
||||||
"""
|
"Payroll Period", payroll_period, ["start_date", "end_date"]
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
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):
|
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");
|
frm.trigger("make_loan_refund");
|
||||||
},__('Create'));
|
},__('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");
|
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) {
|
request_loan_closure: function(frm) {
|
||||||
frappe.confirm(__("Do you really want to close this loan"),
|
frappe.confirm(__("Do you really want to close this loan"),
|
||||||
function() {
|
function() {
|
||||||
|
|||||||
@@ -60,18 +60,20 @@ class Loan(AccountsController):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def validate_cost_center(self):
|
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")
|
self.cost_center = frappe.db.get_value("Company", self.company, "cost_center")
|
||||||
|
|
||||||
if not self.cost_center:
|
if not self.cost_center:
|
||||||
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
|
frappe.throw(_("Cost center is mandatory for loans having rate of interest greater than 0"))
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.link_loan_security_pledge()
|
self.link_loan_security_pledge()
|
||||||
|
# Interest accrual for backdated term loans
|
||||||
|
self.accrue_loan_interest()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.unlink_loan_security_pledge()
|
self.unlink_loan_security_pledge()
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
|
|
||||||
def set_missing_fields(self):
|
def set_missing_fields(self):
|
||||||
if not self.company:
|
if not self.company:
|
||||||
@@ -187,6 +189,16 @@ class Loan(AccountsController):
|
|||||||
|
|
||||||
self.db_set("maximum_loan_amount", maximum_loan_value)
|
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):
|
def unlink_loan_security_pledge(self):
|
||||||
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
|
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
|
||||||
pledge_list = [d.name for d in pledges]
|
pledge_list = [d.name for d in pledges]
|
||||||
@@ -330,6 +342,22 @@ def get_loan_application(loan_application):
|
|||||||
return loan.as_dict()
|
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):
|
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, "total_amount_paid", total_amount_paid)
|
||||||
frappe.db.set_value("Loan", loan, "status", "Closed")
|
frappe.db.set_value("Loan", loan, "status", "Closed")
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ class LoanDisbursement(AccountsController):
|
|||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.set_status_and_amounts(cancel=1)
|
self.set_status_and_amounts(cancel=1)
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
|
|
||||||
def set_missing_values(self):
|
def set_missing_values(self):
|
||||||
if not self.disbursement_date:
|
if not self.disbursement_date:
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ class LoanInterestAccrual(AccountsController):
|
|||||||
self.update_is_accrued()
|
self.update_is_accrued()
|
||||||
|
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
|
|
||||||
def update_is_accrued(self):
|
def update_is_accrued(self):
|
||||||
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
|
frappe.db.set_value("Repayment Schedule", self.repayment_schedule_name, "is_accrued", 0)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class LoanRepayment(AccountsController):
|
|||||||
self.check_future_accruals()
|
self.check_future_accruals()
|
||||||
self.update_repayment_schedule(cancel=1)
|
self.update_repayment_schedule(cancel=1)
|
||||||
self.mark_as_unpaid()
|
self.mark_as_unpaid()
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
|
|
||||||
def set_missing_values(self, amounts):
|
def set_missing_values(self, amounts):
|
||||||
@@ -448,8 +448,6 @@ class LoanRepayment(AccountsController):
|
|||||||
"remarks": remarks,
|
"remarks": remarks,
|
||||||
"cost_center": self.cost_center,
|
"cost_center": self.cost_center,
|
||||||
"posting_date": getdate(self.posting_date),
|
"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 "",
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ class LoanWriteOff(AccountsController):
|
|||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
self.update_outstanding_amount(cancel=1)
|
self.update_outstanding_amount(cancel=1)
|
||||||
self.ignore_linked_doctypes = ["GL Entry"]
|
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
|
||||||
self.make_gl_entries(cancel=1)
|
self.make_gl_entries(cancel=1)
|
||||||
|
|
||||||
def update_outstanding_amount(self, cancel=0):
|
def update_outstanding_amount(self, cancel=0):
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ frappe.ui.form.on("BOM", {
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
if (!frm.doc.__islocal && frm.doc.docstatus<2) {
|
if (!frm.is_new() && frm.doc.docstatus<2) {
|
||||||
frm.add_custom_button(__("Update Cost"), function() {
|
frm.add_custom_button(__("Update Cost"), function() {
|
||||||
frm.events.update_cost(frm, true);
|
frm.events.update_cost(frm, true);
|
||||||
});
|
});
|
||||||
@@ -93,6 +93,13 @@ frappe.ui.form.on("BOM", {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!frm.is_new() && !frm.doc.docstatus == 0) {
|
||||||
|
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) {
|
if(frm.doc.docstatus==1) {
|
||||||
frm.add_custom_button(__("Work Order"), function() {
|
frm.add_custom_button(__("Work Order"), function() {
|
||||||
frm.trigger("make_work_order");
|
frm.trigger("make_work_order");
|
||||||
@@ -499,15 +506,11 @@ cur_frm.cscript.qty = function(doc) {
|
|||||||
|
|
||||||
cur_frm.cscript.rate = function(doc, cdt, cdn) {
|
cur_frm.cscript.rate = function(doc, cdt, cdn) {
|
||||||
var d = locals[cdt][cdn];
|
var d = locals[cdt][cdn];
|
||||||
var scrap_items = false;
|
const is_scrap_item = cdt == "BOM Scrap Item";
|
||||||
|
|
||||||
if(cdt == 'BOM Scrap Item') {
|
|
||||||
scrap_items = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (d.bom_no) {
|
if (d.bom_no) {
|
||||||
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
|
frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
|
||||||
get_bom_material_detail(doc, cdt, cdn, scrap_items);
|
get_bom_material_detail(doc, cdt, cdn, is_scrap_item);
|
||||||
} else {
|
} else {
|
||||||
erpnext.bom.calculate_rm_cost(doc);
|
erpnext.bom.calculate_rm_cost(doc);
|
||||||
erpnext.bom.calculate_scrap_materials_cost(doc);
|
erpnext.bom.calculate_scrap_materials_cost(doc);
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
import re
|
import re
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from operator import itemgetter
|
from operator import itemgetter
|
||||||
from typing import List
|
from typing import Dict, List
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
@@ -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"}
|
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||||
|
|
||||||
|
|
||||||
|
class BOMRecursionError(frappe.ValidationError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class BOMTree:
|
class BOMTree:
|
||||||
"""Full tree representation of a BOM"""
|
"""Full tree representation of a BOM"""
|
||||||
|
|
||||||
@@ -185,6 +189,7 @@ class BOM(WebsiteGenerator):
|
|||||||
self.validate_transfer_against()
|
self.validate_transfer_against()
|
||||||
self.set_routing_operations()
|
self.set_routing_operations()
|
||||||
self.validate_operations()
|
self.validate_operations()
|
||||||
|
self.update_exploded_items(save=False)
|
||||||
self.calculate_cost()
|
self.calculate_cost()
|
||||||
self.update_stock_qty()
|
self.update_stock_qty()
|
||||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||||
@@ -251,9 +256,8 @@ class BOM(WebsiteGenerator):
|
|||||||
for item in self.get("items"):
|
for item in self.get("items"):
|
||||||
self.validate_bom_currency(item)
|
self.validate_bom_currency(item)
|
||||||
|
|
||||||
item.bom_no = ""
|
if item.do_not_explode:
|
||||||
if not item.do_not_explode:
|
item.bom_no = ""
|
||||||
item.bom_no = item.bom_no
|
|
||||||
|
|
||||||
ret = self.get_bom_material_detail(
|
ret = self.get_bom_material_detail(
|
||||||
{
|
{
|
||||||
@@ -383,40 +387,14 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
existing_bom_cost = self.total_cost
|
existing_bom_cost = self.total_cost
|
||||||
|
|
||||||
for d in self.get("items"):
|
|
||||||
if not d.item_code:
|
|
||||||
continue
|
|
||||||
|
|
||||||
rate = self.get_rm_rate(
|
|
||||||
{
|
|
||||||
"company": self.company,
|
|
||||||
"item_code": d.item_code,
|
|
||||||
"bom_no": d.bom_no,
|
|
||||||
"qty": d.qty,
|
|
||||||
"uom": d.uom,
|
|
||||||
"stock_uom": d.stock_uom,
|
|
||||||
"conversion_factor": d.conversion_factor,
|
|
||||||
"sourced_by_supplier": d.sourced_by_supplier,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
if rate:
|
|
||||||
d.rate = rate
|
|
||||||
d.amount = flt(d.rate) * flt(d.qty)
|
|
||||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
|
||||||
d.base_amount = flt(d.amount) * flt(self.conversion_rate)
|
|
||||||
|
|
||||||
if save:
|
|
||||||
d.db_update()
|
|
||||||
|
|
||||||
if self.docstatus == 1:
|
if self.docstatus == 1:
|
||||||
self.flags.ignore_validate_update_after_submit = True
|
self.flags.ignore_validate_update_after_submit = True
|
||||||
self.calculate_cost(update_hour_rate)
|
|
||||||
|
self.calculate_cost(save_updates=save, update_hour_rate=update_hour_rate)
|
||||||
|
|
||||||
if save:
|
if save:
|
||||||
self.db_update()
|
self.db_update()
|
||||||
|
|
||||||
self.update_exploded_items(save=save)
|
|
||||||
|
|
||||||
# update parent BOMs
|
# update parent BOMs
|
||||||
if self.total_cost != existing_bom_cost and update_parent:
|
if self.total_cost != existing_bom_cost and update_parent:
|
||||||
parent_boms = frappe.db.sql_list(
|
parent_boms = frappe.db.sql_list(
|
||||||
@@ -555,35 +533,27 @@ class BOM(WebsiteGenerator):
|
|||||||
"""Check whether recursion occurs in any bom"""
|
"""Check whether recursion occurs in any bom"""
|
||||||
|
|
||||||
def _throw_error(bom_name):
|
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()
|
bom_list = self.traverse_tree()
|
||||||
child_items = (
|
child_items = frappe.get_all(
|
||||||
frappe.get_all(
|
"BOM Item",
|
||||||
"BOM Item",
|
fields=["bom_no", "item_code"],
|
||||||
fields=["bom_no", "item_code"],
|
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
|
||||||
)
|
|
||||||
or []
|
|
||||||
)
|
)
|
||||||
|
|
||||||
child_bom = {d.bom_no for d in child_items}
|
for item in child_items:
|
||||||
child_items_codes = {d.item_code for d 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:
|
if self.name in {d.bom_no for d in self.items}:
|
||||||
_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}:
|
|
||||||
_throw_error(self.name)
|
_throw_error(self.name)
|
||||||
|
|
||||||
def traverse_tree(self, bom_list=None):
|
def traverse_tree(self, bom_list=None):
|
||||||
@@ -613,11 +583,15 @@ class BOM(WebsiteGenerator):
|
|||||||
bom_list.reverse()
|
bom_list.reverse()
|
||||||
return bom_list
|
return bom_list
|
||||||
|
|
||||||
def calculate_cost(self, update_hour_rate=False):
|
def calculate_cost(self, save_updates=False, update_hour_rate=False):
|
||||||
"""Calculate bom totals"""
|
"""Calculate bom totals"""
|
||||||
self.calculate_op_cost(update_hour_rate)
|
self.calculate_op_cost(update_hour_rate)
|
||||||
self.calculate_rm_cost()
|
self.calculate_rm_cost(save=save_updates)
|
||||||
self.calculate_sm_cost()
|
self.calculate_sm_cost(save=save_updates)
|
||||||
|
if save_updates:
|
||||||
|
# not via doc event, table is not regenerated and needs updation
|
||||||
|
self.calculate_exploded_cost()
|
||||||
|
|
||||||
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
self.total_cost = self.operating_cost + self.raw_material_cost - self.scrap_material_cost
|
||||||
self.base_total_cost = (
|
self.base_total_cost = (
|
||||||
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
self.base_operating_cost + self.base_raw_material_cost - self.base_scrap_material_cost
|
||||||
@@ -659,12 +633,26 @@ class BOM(WebsiteGenerator):
|
|||||||
if update_hour_rate:
|
if update_hour_rate:
|
||||||
row.db_update()
|
row.db_update()
|
||||||
|
|
||||||
def calculate_rm_cost(self):
|
def calculate_rm_cost(self, save=False):
|
||||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||||
total_rm_cost = 0
|
total_rm_cost = 0
|
||||||
base_total_rm_cost = 0
|
base_total_rm_cost = 0
|
||||||
|
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
|
old_rate = d.rate
|
||||||
|
d.rate = self.get_rm_rate(
|
||||||
|
{
|
||||||
|
"company": self.company,
|
||||||
|
"item_code": d.item_code,
|
||||||
|
"bom_no": d.bom_no,
|
||||||
|
"qty": d.qty,
|
||||||
|
"uom": d.uom,
|
||||||
|
"stock_uom": d.stock_uom,
|
||||||
|
"conversion_factor": d.conversion_factor,
|
||||||
|
"sourced_by_supplier": d.sourced_by_supplier,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
d.base_rate = flt(d.rate) * flt(self.conversion_rate)
|
||||||
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
d.amount = flt(d.rate, d.precision("rate")) * flt(d.qty, d.precision("qty"))
|
||||||
d.base_amount = d.amount * flt(self.conversion_rate)
|
d.base_amount = d.amount * flt(self.conversion_rate)
|
||||||
@@ -674,11 +662,13 @@ class BOM(WebsiteGenerator):
|
|||||||
|
|
||||||
total_rm_cost += d.amount
|
total_rm_cost += d.amount
|
||||||
base_total_rm_cost += d.base_amount
|
base_total_rm_cost += d.base_amount
|
||||||
|
if save and (old_rate != d.rate):
|
||||||
|
d.db_update()
|
||||||
|
|
||||||
self.raw_material_cost = total_rm_cost
|
self.raw_material_cost = total_rm_cost
|
||||||
self.base_raw_material_cost = base_total_rm_cost
|
self.base_raw_material_cost = base_total_rm_cost
|
||||||
|
|
||||||
def calculate_sm_cost(self):
|
def calculate_sm_cost(self, save=False):
|
||||||
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
"""Fetch RM rate as per today's valuation rate and calculate totals"""
|
||||||
total_sm_cost = 0
|
total_sm_cost = 0
|
||||||
base_total_sm_cost = 0
|
base_total_sm_cost = 0
|
||||||
@@ -693,10 +683,45 @@ class BOM(WebsiteGenerator):
|
|||||||
)
|
)
|
||||||
total_sm_cost += d.amount
|
total_sm_cost += d.amount
|
||||||
base_total_sm_cost += d.base_amount
|
base_total_sm_cost += d.base_amount
|
||||||
|
if save:
|
||||||
|
d.db_update()
|
||||||
|
|
||||||
self.scrap_material_cost = total_sm_cost
|
self.scrap_material_cost = total_sm_cost
|
||||||
self.base_scrap_material_cost = base_total_sm_cost
|
self.base_scrap_material_cost = base_total_sm_cost
|
||||||
|
|
||||||
|
def calculate_exploded_cost(self):
|
||||||
|
"Set exploded row cost from it's parent BOM."
|
||||||
|
rm_rate_map = self.get_rm_rate_map()
|
||||||
|
|
||||||
|
for row in self.get("exploded_items"):
|
||||||
|
old_rate = flt(row.rate)
|
||||||
|
row.rate = rm_rate_map.get(row.item_code)
|
||||||
|
row.amount = flt(row.stock_qty) * flt(row.rate)
|
||||||
|
|
||||||
|
if old_rate != row.rate:
|
||||||
|
# Only db_update if changed
|
||||||
|
row.db_update()
|
||||||
|
|
||||||
|
def get_rm_rate_map(self) -> Dict[str, float]:
|
||||||
|
"Create Raw Material-Rate map for Exploded Items. Fetch rate from Items table or Subassembly BOM."
|
||||||
|
rm_rate_map = {}
|
||||||
|
|
||||||
|
for item in self.get("items"):
|
||||||
|
if item.bom_no:
|
||||||
|
# Get Item-Rate from Subassembly BOM
|
||||||
|
explosion_items = frappe.get_all(
|
||||||
|
"BOM Explosion Item",
|
||||||
|
filters={"parent": item.bom_no},
|
||||||
|
fields=["item_code", "rate"],
|
||||||
|
order_by=None, # to avoid sort index creation at db level (granular change)
|
||||||
|
)
|
||||||
|
explosion_item_rate = {item.item_code: flt(item.rate) for item in explosion_items}
|
||||||
|
rm_rate_map.update(explosion_item_rate)
|
||||||
|
else:
|
||||||
|
rm_rate_map[item.item_code] = flt(item.base_rate) / flt(item.conversion_factor or 1.0)
|
||||||
|
|
||||||
|
return rm_rate_map
|
||||||
|
|
||||||
def update_exploded_items(self, save=True):
|
def update_exploded_items(self, save=True):
|
||||||
"""Update Flat BOM, following will be correct data"""
|
"""Update Flat BOM, following will be correct data"""
|
||||||
self.get_exploded_items()
|
self.get_exploded_items()
|
||||||
@@ -907,44 +932,46 @@ def get_bom_item_rate(args, bom_doc):
|
|||||||
return flt(rate)
|
return flt(rate)
|
||||||
|
|
||||||
|
|
||||||
def get_valuation_rate(args):
|
def get_valuation_rate(data):
|
||||||
"""Get weighted average of valuation rate from all warehouses"""
|
"""
|
||||||
|
1) Get average valuation rate from all warehouses
|
||||||
|
2) If no value, get last valuation rate from SLE
|
||||||
|
3) If no value, get valuation rate from Item
|
||||||
|
"""
|
||||||
|
from frappe.query_builder.functions import Sum
|
||||||
|
|
||||||
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
|
item_code, company = data.get("item_code"), data.get("company")
|
||||||
item_bins = frappe.db.sql(
|
valuation_rate = 0.0
|
||||||
"""
|
|
||||||
select
|
|
||||||
bin.actual_qty, bin.stock_value
|
|
||||||
from
|
|
||||||
`tabBin` bin, `tabWarehouse` warehouse
|
|
||||||
where
|
|
||||||
bin.item_code=%(item)s
|
|
||||||
and bin.warehouse = warehouse.name
|
|
||||||
and warehouse.company=%(company)s""",
|
|
||||||
{"item": args["item_code"], "company": args["company"]},
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
for d in item_bins:
|
bin_table = frappe.qb.DocType("Bin")
|
||||||
total_qty += flt(d.actual_qty)
|
wh_table = frappe.qb.DocType("Warehouse")
|
||||||
total_value += flt(d.stock_value)
|
item_valuation = (
|
||||||
|
frappe.qb.from_(bin_table)
|
||||||
|
.join(wh_table)
|
||||||
|
.on(bin_table.warehouse == wh_table.name)
|
||||||
|
.select((Sum(bin_table.stock_value) / Sum(bin_table.actual_qty)).as_("valuation_rate"))
|
||||||
|
.where((bin_table.item_code == item_code) & (wh_table.company == company))
|
||||||
|
).run(as_dict=True)[0]
|
||||||
|
|
||||||
if total_qty:
|
valuation_rate = item_valuation.get("valuation_rate")
|
||||||
valuation_rate = total_value / total_qty
|
|
||||||
|
|
||||||
if valuation_rate <= 0:
|
if (valuation_rate is not None) and valuation_rate <= 0:
|
||||||
last_valuation_rate = frappe.db.sql(
|
# Explicit null value check. If None, Bins don't exist, neither does SLE
|
||||||
"""select valuation_rate
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
from `tabStock Ledger Entry`
|
last_val_rate = (
|
||||||
where item_code = %s and valuation_rate > 0 and is_cancelled = 0
|
frappe.qb.from_(sle)
|
||||||
order by posting_date desc, posting_time desc, creation desc limit 1""",
|
.select(sle.valuation_rate)
|
||||||
args["item_code"],
|
.where((sle.item_code == item_code) & (sle.valuation_rate > 0) & (sle.is_cancelled == 0))
|
||||||
)
|
.orderby(sle.posting_date, order=frappe.qb.desc)
|
||||||
|
.orderby(sle.posting_time, order=frappe.qb.desc)
|
||||||
|
.orderby(sle.creation, order=frappe.qb.desc)
|
||||||
|
.limit(1)
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
valuation_rate = flt(last_valuation_rate[0][0]) if last_valuation_rate else 0
|
valuation_rate = flt(last_val_rate[0].get("valuation_rate")) if last_val_rate else 0
|
||||||
|
|
||||||
if not valuation_rate:
|
if not valuation_rate:
|
||||||
valuation_rate = frappe.db.get_value("Item", args["item_code"], "valuation_rate")
|
valuation_rate = frappe.db.get_value("Item", item_code, "valuation_rate")
|
||||||
|
|
||||||
return flt(valuation_rate)
|
return flt(valuation_rate)
|
||||||
|
|
||||||
@@ -1130,39 +1157,6 @@ def get_children(parent=None, is_root=False, **filters):
|
|||||||
return bom_items
|
return bom_items
|
||||||
|
|
||||||
|
|
||||||
def get_boms_in_bottom_up_order(bom_no=None):
|
|
||||||
def _get_parent(bom_no):
|
|
||||||
return frappe.db.sql_list(
|
|
||||||
"""
|
|
||||||
select distinct bom_item.parent from `tabBOM Item` bom_item
|
|
||||||
where bom_item.bom_no = %s and bom_item.docstatus=1 and bom_item.parenttype='BOM'
|
|
||||||
and exists(select bom.name from `tabBOM` bom where bom.name=bom_item.parent and bom.is_active=1)
|
|
||||||
""",
|
|
||||||
bom_no,
|
|
||||||
)
|
|
||||||
|
|
||||||
count = 0
|
|
||||||
bom_list = []
|
|
||||||
if bom_no:
|
|
||||||
bom_list.append(bom_no)
|
|
||||||
else:
|
|
||||||
# get all leaf BOMs
|
|
||||||
bom_list = frappe.db.sql_list(
|
|
||||||
"""select name from `tabBOM` bom
|
|
||||||
where docstatus=1 and is_active=1
|
|
||||||
and not exists(select bom_no from `tabBOM Item`
|
|
||||||
where parent=bom.name and ifnull(bom_no, '')!='')"""
|
|
||||||
)
|
|
||||||
|
|
||||||
while count < len(bom_list):
|
|
||||||
for child_bom in _get_parent(bom_list[count]):
|
|
||||||
if child_bom not in bom_list:
|
|
||||||
bom_list.append(child_bom)
|
|
||||||
count += 1
|
|
||||||
|
|
||||||
return bom_list
|
|
||||||
|
|
||||||
|
|
||||||
def add_additional_cost(stock_entry, work_order):
|
def add_additional_cost(stock_entry, work_order):
|
||||||
# Add non stock items cost in the additional cost
|
# Add non stock items cost in the additional cost
|
||||||
stock_entry.additional_costs = []
|
stock_entry.additional_costs = []
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ from frappe.tests.utils import FrappeTestCase
|
|||||||
from frappe.utils import cstr, flt
|
from frappe.utils import cstr, flt
|
||||||
|
|
||||||
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
|
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_tool.bom_update_tool import update_cost
|
from erpnext.manufacturing.doctype.bom_update_log.test_bom_update_log import (
|
||||||
|
update_cost_in_all_boms_in_test,
|
||||||
|
)
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
@@ -69,26 +71,31 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
def test_update_bom_cost_in_all_boms(self):
|
def test_update_bom_cost_in_all_boms(self):
|
||||||
# get current rate for '_Test Item 2'
|
# get current rate for '_Test Item 2'
|
||||||
rm_rate = frappe.db.sql(
|
bom_rates = frappe.db.get_values(
|
||||||
"""select rate from `tabBOM Item`
|
"BOM Item",
|
||||||
where parent='BOM-_Test Item Home Desktop Manufactured-001'
|
{
|
||||||
and item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'"""
|
"parent": "BOM-_Test Item Home Desktop Manufactured-001",
|
||||||
|
"item_code": "_Test Item 2",
|
||||||
|
"docstatus": 1,
|
||||||
|
},
|
||||||
|
fieldname=["rate", "base_rate"],
|
||||||
|
as_dict=True,
|
||||||
)
|
)
|
||||||
rm_rate = rm_rate[0][0] if rm_rate else 0
|
rm_base_rate = bom_rates[0].get("base_rate") if bom_rates else 0
|
||||||
|
|
||||||
# Reset item valuation rate
|
# Reset item valuation rate
|
||||||
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_rate + 10)
|
reset_item_valuation_rate(item_code="_Test Item 2", qty=200, rate=rm_base_rate + 10)
|
||||||
|
|
||||||
# update cost of all BOMs based on latest valuation rate
|
# update cost of all BOMs based on latest valuation rate
|
||||||
update_cost()
|
update_cost_in_all_boms_in_test()
|
||||||
|
|
||||||
# check if new valuation rate updated in all BOMs
|
# check if new valuation rate updated in all BOMs
|
||||||
for d in frappe.db.sql(
|
for d in frappe.db.sql(
|
||||||
"""select rate from `tabBOM Item`
|
"""select base_rate from `tabBOM Item`
|
||||||
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
|
where item_code='_Test Item 2' and docstatus=1 and parenttype='BOM'""",
|
||||||
as_dict=1,
|
as_dict=1,
|
||||||
):
|
):
|
||||||
self.assertEqual(d.rate, rm_rate + 10)
|
self.assertEqual(d.base_rate, rm_base_rate + 10)
|
||||||
|
|
||||||
def test_bom_cost(self):
|
def test_bom_cost(self):
|
||||||
bom = frappe.copy_doc(test_records[2])
|
bom = frappe.copy_doc(test_records[2])
|
||||||
@@ -324,43 +331,36 @@ class TestBOM(FrappeTestCase):
|
|||||||
|
|
||||||
def test_bom_recursion_1st_level(self):
|
def test_bom_recursion_1st_level(self):
|
||||||
"""BOM should not allow BOM item again in child"""
|
"""BOM should not allow BOM item again in child"""
|
||||||
item_code = "_Test BOM Recursion"
|
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_item(item_code, {"is_stock_item": 1})
|
|
||||||
|
|
||||||
bom = frappe.new_doc("BOM")
|
bom = frappe.new_doc("BOM")
|
||||||
bom.item = item_code
|
bom.item = item_code
|
||||||
bom.append("items", frappe._dict(item_code=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()
|
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):
|
def test_bom_recursion_transitive(self):
|
||||||
item1 = "_Test BOM Recursion"
|
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||||
item2 = "_Test BOM Recursion 2"
|
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||||
make_item(item1, {"is_stock_item": 1})
|
|
||||||
make_item(item2, {"is_stock_item": 1})
|
|
||||||
|
|
||||||
bom1 = frappe.new_doc("BOM")
|
bom1 = frappe.new_doc("BOM")
|
||||||
bom1.item = item1
|
bom1.item = item1
|
||||||
bom1.append("items", frappe._dict(item_code=item2))
|
bom1.append("items", frappe._dict(item_code=item2))
|
||||||
bom1.save()
|
bom1.save()
|
||||||
bom1.submit()
|
|
||||||
|
|
||||||
bom2 = frappe.new_doc("BOM")
|
bom2 = frappe.new_doc("BOM")
|
||||||
bom2.item = item2
|
bom2.item = item2
|
||||||
bom2.append("items", frappe._dict(item_code=item1))
|
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.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):
|
def test_bom_with_process_loss_item(self):
|
||||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||||
|
|||||||
@@ -32,6 +32,7 @@
|
|||||||
"is_active": 1,
|
"is_active": 1,
|
||||||
"is_default": 1,
|
"is_default": 1,
|
||||||
"item": "_Test Item Home Desktop Manufactured",
|
"item": "_Test Item Home Desktop Manufactured",
|
||||||
|
"company": "_Test Company",
|
||||||
"quantity": 1.0
|
"quantity": 1.0
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -169,13 +169,15 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-10-08 16:21:29.386212",
|
"modified": "2022-05-27 13:42:23.305455",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Explosion Item",
|
"name": "BOM Explosion Item",
|
||||||
|
"naming_rule": "Random",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
||||||
@@ -33,7 +33,6 @@
|
|||||||
"amount",
|
"amount",
|
||||||
"base_amount",
|
"base_amount",
|
||||||
"section_break_18",
|
"section_break_18",
|
||||||
"scrap",
|
|
||||||
"qty_consumed_per_unit",
|
"qty_consumed_per_unit",
|
||||||
"section_break_27",
|
"section_break_27",
|
||||||
"has_variants",
|
"has_variants",
|
||||||
@@ -223,15 +222,6 @@
|
|||||||
"fieldname": "section_break_18",
|
"fieldname": "section_break_18",
|
||||||
"fieldtype": "Section Break"
|
"fieldtype": "Section Break"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"columns": 1,
|
|
||||||
"fieldname": "scrap",
|
|
||||||
"fieldtype": "Float",
|
|
||||||
"label": "Scrap %",
|
|
||||||
"oldfieldname": "scrap",
|
|
||||||
"oldfieldtype": "Currency",
|
|
||||||
"print_hide": 1
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"fieldname": "qty_consumed_per_unit",
|
"fieldname": "qty_consumed_per_unit",
|
||||||
"fieldtype": "Float",
|
"fieldtype": "Float",
|
||||||
@@ -298,7 +288,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-01-24 16:57:57.020232",
|
"modified": "2022-05-19 02:32:43.785470",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Item",
|
"name": "BOM Item",
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
{
|
||||||
|
"actions": [],
|
||||||
|
"autoname": "autoincrement",
|
||||||
|
"creation": "2022-05-31 17:34:39.825537",
|
||||||
|
"doctype": "DocType",
|
||||||
|
"engine": "InnoDB",
|
||||||
|
"field_order": [
|
||||||
|
"level",
|
||||||
|
"batch_no",
|
||||||
|
"boms_updated",
|
||||||
|
"status"
|
||||||
|
],
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"fieldname": "level",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Level"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "batch_no",
|
||||||
|
"fieldtype": "Int",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Batch No."
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "boms_updated",
|
||||||
|
"fieldtype": "Long Text",
|
||||||
|
"hidden": 1,
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "BOMs Updated"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Status",
|
||||||
|
"options": "Pending\nCompleted",
|
||||||
|
"read_only": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"index_web_pages_for_search": 1,
|
||||||
|
"istable": 1,
|
||||||
|
"links": [],
|
||||||
|
"modified": "2022-06-06 14:50:35.161062",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Manufacturing",
|
||||||
|
"name": "BOM Update Batch",
|
||||||
|
"naming_rule": "Autoincrement",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"permissions": [],
|
||||||
|
"sort_field": "modified",
|
||||||
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
# import frappe
|
||||||
|
from frappe.model.document import Document
|
||||||
|
|
||||||
|
|
||||||
|
class BOMUpdateBatch(Document):
|
||||||
|
pass
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user