Merge branch 'develop' into subcontracting
This commit is contained in:
commit
992ba974e2
2
.github/helper/install.sh
vendored
2
.github/helper/install.sh
vendored
@ -11,7 +11,7 @@ fi
|
||||
|
||||
cd ~ || exit
|
||||
|
||||
sudo apt-get install redis-server libcups2-dev
|
||||
sudo apt update && sudo apt install redis-server libcups2-dev
|
||||
|
||||
pip install frappe-bench
|
||||
|
||||
|
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": [
|
||||
"**/*.*"
|
||||
]
|
||||
}
|
@ -102,7 +102,9 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
});
|
||||
},
|
||||
|
||||
before_save: function(frm) {
|
||||
before_save: async function(frm) {
|
||||
frappe.dom.freeze(__('Processing Sales! Please Wait...'));
|
||||
|
||||
frm.set_value("grand_total", 0);
|
||||
frm.set_value("net_total", 0);
|
||||
frm.set_value("total_quantity", 0);
|
||||
@ -112,17 +114,23 @@ frappe.ui.form.on('POS Closing Entry', {
|
||||
row.expected_amount = row.opening_amount;
|
||||
}
|
||||
|
||||
for (let row of frm.doc.pos_transactions) {
|
||||
frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
});
|
||||
const pos_inv_promises = frm.doc.pos_transactions.map(
|
||||
row => frappe.db.get_doc("POS Invoice", row.pos_invoice)
|
||||
);
|
||||
|
||||
const pos_invoices = await Promise.all(pos_inv_promises);
|
||||
|
||||
for (let doc of pos_invoices) {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
}
|
||||
|
||||
frappe.dom.unfreeze();
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -211,6 +211,7 @@ def set_gl_entries_by_account(
|
||||
{additional_conditions}
|
||||
and posting_date <= %(to_date)s
|
||||
and {based_on} is not null
|
||||
and is_cancelled = 0
|
||||
order by {based_on}, posting_date""".format(
|
||||
additional_conditions="\n".join(additional_conditions), based_on=based_on
|
||||
),
|
||||
|
@ -148,7 +148,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-14 15:56:42.340223",
|
||||
"modified": "2022-05-31 19:40:26.103909",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
@ -162,6 +162,16 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
@ -18,7 +18,7 @@ class BuyingSettings(Document):
|
||||
for key in ["supplier_group", "supp_master_name", "maintain_same_rate", "buying_price_list"]:
|
||||
frappe.db.set_default(key, self.get(key, ""))
|
||||
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
from erpnext.utilities.naming import set_by_naming_series
|
||||
|
||||
set_by_naming_series(
|
||||
"Supplier",
|
||||
|
@ -1866,7 +1866,7 @@ def get_default_taxes_and_charges(master_doctype, tax_template=None, company=Non
|
||||
def get_taxes_and_charges(master_doctype, master_name):
|
||||
if not master_name:
|
||||
return
|
||||
from frappe.model import default_fields
|
||||
from frappe.model import child_table_fields, default_fields
|
||||
|
||||
tax_master = frappe.get_doc(master_doctype, master_name)
|
||||
|
||||
@ -1874,7 +1874,7 @@ def get_taxes_and_charges(master_doctype, master_name):
|
||||
for i, tax in enumerate(tax_master.get("taxes")):
|
||||
tax = tax.as_dict()
|
||||
|
||||
for fieldname in default_fields:
|
||||
for fieldname in default_fields + child_table_fields:
|
||||
if fieldname in tax:
|
||||
del tax[fieldname]
|
||||
|
||||
@ -2657,7 +2657,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil
|
||||
parent.update_ordered_qty()
|
||||
parent.update_ordered_and_reserved_qty()
|
||||
parent.update_receiving_percentage()
|
||||
else:
|
||||
else: # Sales Order
|
||||
parent.validate_warehouse()
|
||||
parent.update_reserved_qty()
|
||||
parent.update_project()
|
||||
parent.update_prevdoc_status("submit")
|
||||
|
@ -34,9 +34,7 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
def autoname(self):
|
||||
# use naming series to accomodate items with same name (different item code)
|
||||
from frappe.model.naming import make_autoname
|
||||
|
||||
from erpnext.setup.doctype.naming_series.naming_series import get_default_naming_series
|
||||
from frappe.model.naming import get_default_naming_series, make_autoname
|
||||
|
||||
naming_series = get_default_naming_series("Website Item")
|
||||
if not self.name and naming_series:
|
||||
|
@ -3,7 +3,15 @@
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, get_year_ending, get_year_start, getdate, now_datetime, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
add_months,
|
||||
get_last_day,
|
||||
get_year_ending,
|
||||
get_year_start,
|
||||
getdate,
|
||||
nowdate,
|
||||
)
|
||||
|
||||
from erpnext.hr.doctype.attendance.attendance import (
|
||||
DuplicateAttendanceError,
|
||||
@ -138,69 +146,70 @@ class TestAttendance(FrappeTestCase):
|
||||
self.assertEqual(attendance, fetch_attendance)
|
||||
|
||||
def test_unmarked_days(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
attendance_date = add_days(first_sunday, 1)
|
||||
|
||||
employee = make_employee(
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||
)
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
|
||||
mark_attendance(employee, first_day, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||
# holiday considered in unmarked days
|
||||
self.assertIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_excluding_holidays(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
attendance_date = add_days(first_sunday, 1)
|
||||
|
||||
employee = make_employee(
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(first_day, -1)
|
||||
"test_unmarked_days@example.com", date_of_joining=add_days(attendance_date, -1)
|
||||
)
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
first_sunday = get_first_sunday(self.holiday_list, for_date=first_day)
|
||||
mark_attendance(employee, first_day, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name, exclude_holidays=True)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
||||
# attendance already marked for the day
|
||||
self.assertNotIn(first_day, unmarked_days)
|
||||
self.assertNotIn(attendance_date, unmarked_days)
|
||||
# attendance unmarked
|
||||
self.assertIn(getdate(add_days(first_day, 1)), unmarked_days)
|
||||
self.assertIn(getdate(add_days(attendance_date, 1)), unmarked_days)
|
||||
# holidays not considered in unmarked days
|
||||
self.assertNotIn(first_sunday, unmarked_days)
|
||||
|
||||
def test_unmarked_days_as_per_joining_and_relieving_dates(self):
|
||||
now = now_datetime()
|
||||
previous_month = now.month - 1
|
||||
first_day = now.replace(day=1).replace(month=previous_month).date()
|
||||
first_sunday = get_first_sunday(
|
||||
self.holiday_list, for_date=get_last_day(add_months(getdate(), -1))
|
||||
)
|
||||
date = add_days(first_sunday, 1)
|
||||
|
||||
doj = add_days(first_day, 1)
|
||||
relieving_date = add_days(first_day, 5)
|
||||
doj = add_days(date, 1)
|
||||
relieving_date = add_days(date, 5)
|
||||
employee = make_employee(
|
||||
"test_unmarked_days_as_per_doj@example.com", date_of_joining=doj, relieving_date=relieving_date
|
||||
)
|
||||
|
||||
frappe.db.set_value("Employee", employee, "holiday_list", self.holiday_list)
|
||||
|
||||
attendance_date = add_days(first_day, 2)
|
||||
attendance_date = add_days(date, 2)
|
||||
mark_attendance(employee, attendance_date, "Present")
|
||||
month_name = get_month_name(first_day)
|
||||
month_name = get_month_name(attendance_date)
|
||||
|
||||
unmarked_days = get_unmarked_days(employee, month_name)
|
||||
unmarked_days = [getdate(date) for date in unmarked_days]
|
||||
|
@ -230,7 +230,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
persons_name = anniversary_person
|
||||
# Number of years completed at the company
|
||||
completed_years = getdate().year - anniversary_persons[0]["date_of_joining"].year
|
||||
anniversary_person += f" completed {completed_years} year(s)"
|
||||
anniversary_person += f" completed {get_pluralized_years(completed_years)}"
|
||||
else:
|
||||
person_names_with_years = []
|
||||
names = []
|
||||
@ -239,7 +239,7 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
names.append(person_text)
|
||||
# Number of years completed at the company
|
||||
completed_years = getdate().year - person["date_of_joining"].year
|
||||
person_text += f" completed {completed_years} year(s)"
|
||||
person_text += f" completed {get_pluralized_years(completed_years)}"
|
||||
person_names_with_years.append(person_text)
|
||||
|
||||
# converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
|
||||
@ -254,6 +254,12 @@ def get_work_anniversary_reminder_text_and_message(anniversary_persons):
|
||||
return reminder_text, message
|
||||
|
||||
|
||||
def get_pluralized_years(years):
|
||||
if years == 1:
|
||||
return "1 year"
|
||||
return f"{years} years"
|
||||
|
||||
|
||||
def send_work_anniversary_reminder(recipients, reminder_text, anniversary_persons, message):
|
||||
frappe.sendmail(
|
||||
recipients=recipients,
|
||||
|
@ -22,7 +22,7 @@ class HRSettings(Document):
|
||||
PROCEED_WITH_FREQUENCY_CHANGE = False
|
||||
|
||||
def set_naming_series(self):
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
from erpnext.utilities.naming import set_by_naming_series
|
||||
|
||||
set_by_naming_series(
|
||||
"Employee",
|
||||
|
@ -439,20 +439,18 @@ def check_effective_date(from_date, to_date, frequency, based_on_date_of_joining
|
||||
return False
|
||||
|
||||
|
||||
def get_salary_assignment(employee, date):
|
||||
assignment = frappe.db.sql(
|
||||
"""
|
||||
select * from `tabSalary Structure Assignment`
|
||||
where employee=%(employee)s
|
||||
and docstatus = 1
|
||||
and %(on_date)s >= from_date order by from_date desc limit 1""",
|
||||
{
|
||||
"employee": employee,
|
||||
"on_date": date,
|
||||
},
|
||||
as_dict=1,
|
||||
def get_salary_assignments(employee, payroll_period):
|
||||
start_date, end_date = frappe.db.get_value(
|
||||
"Payroll Period", payroll_period, ["start_date", "end_date"]
|
||||
)
|
||||
return assignment[0] if assignment else None
|
||||
assignments = frappe.db.get_all(
|
||||
"Salary Structure Assignment",
|
||||
filters={"employee": employee, "docstatus": 1, "from_date": ["between", (start_date, end_date)]},
|
||||
fields=["*"],
|
||||
order_by="from_date",
|
||||
)
|
||||
|
||||
return assignments
|
||||
|
||||
|
||||
def get_sal_slip_total_benefit_given(employee, payroll_period, component=False):
|
||||
|
@ -68,6 +68,8 @@ class Loan(AccountsController):
|
||||
|
||||
def on_submit(self):
|
||||
self.link_loan_security_pledge()
|
||||
# Interest accrual for backdated term loans
|
||||
self.accrue_loan_interest()
|
||||
|
||||
def on_cancel(self):
|
||||
self.unlink_loan_security_pledge()
|
||||
@ -187,6 +189,16 @@ class Loan(AccountsController):
|
||||
|
||||
self.db_set("maximum_loan_amount", maximum_loan_value)
|
||||
|
||||
def accrue_loan_interest(self):
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_term_loans,
|
||||
)
|
||||
|
||||
if getdate(self.repayment_start_date) < getdate() and self.is_term_loan:
|
||||
process_loan_interest_accrual_for_term_loans(
|
||||
posting_date=getdate(), loan_type=self.loan_type, loan=self.name
|
||||
)
|
||||
|
||||
def unlink_loan_security_pledge(self):
|
||||
pledges = frappe.get_all("Loan Security Pledge", fields=["name"], filters={"loan": self.name})
|
||||
pledge_list = [d.name for d in pledges]
|
||||
|
@ -93,6 +93,11 @@ frappe.ui.form.on("BOM", {
|
||||
});
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("New Version"), function() {
|
||||
let new_bom = frappe.model.copy_doc(frm.doc);
|
||||
frappe.set_route("Form", "BOM", new_bom.name);
|
||||
});
|
||||
|
||||
if(frm.doc.docstatus==1) {
|
||||
frm.add_custom_button(__("Work Order"), function() {
|
||||
frm.trigger("make_work_order");
|
||||
|
@ -22,6 +22,10 @@ from erpnext.stock.get_item_details import get_conversion_factor, get_price_list
|
||||
form_grid_templates = {"items": "templates/form_grid/item_grid.html"}
|
||||
|
||||
|
||||
class BOMRecursionError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class BOMTree:
|
||||
"""Full tree representation of a BOM"""
|
||||
|
||||
@ -251,9 +255,8 @@ class BOM(WebsiteGenerator):
|
||||
for item in self.get("items"):
|
||||
self.validate_bom_currency(item)
|
||||
|
||||
item.bom_no = ""
|
||||
if not item.do_not_explode:
|
||||
item.bom_no = item.bom_no
|
||||
if item.do_not_explode:
|
||||
item.bom_no = ""
|
||||
|
||||
ret = self.get_bom_material_detail(
|
||||
{
|
||||
@ -555,35 +558,27 @@ class BOM(WebsiteGenerator):
|
||||
"""Check whether recursion occurs in any bom"""
|
||||
|
||||
def _throw_error(bom_name):
|
||||
frappe.throw(_("BOM recursion: {0} cannot be parent or child of {0}").format(bom_name))
|
||||
frappe.throw(
|
||||
_("BOM recursion: {1} cannot be parent or child of {0}").format(self.name, bom_name),
|
||||
exc=BOMRecursionError,
|
||||
)
|
||||
|
||||
bom_list = self.traverse_tree()
|
||||
child_items = (
|
||||
frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["bom_no", "item_code"],
|
||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||
)
|
||||
or []
|
||||
child_items = frappe.get_all(
|
||||
"BOM Item",
|
||||
fields=["bom_no", "item_code"],
|
||||
filters={"parent": ("in", bom_list), "parenttype": "BOM"},
|
||||
)
|
||||
|
||||
child_bom = {d.bom_no for d in child_items}
|
||||
child_items_codes = {d.item_code for d in child_items}
|
||||
for item in child_items:
|
||||
if self.name == item.bom_no:
|
||||
_throw_error(self.name)
|
||||
if self.item == item.item_code and item.bom_no:
|
||||
# Same item but with different BOM should not be allowed.
|
||||
# Same item can appear recursively once as long as it doesn't have BOM.
|
||||
_throw_error(item.bom_no)
|
||||
|
||||
if self.name in child_bom:
|
||||
_throw_error(self.name)
|
||||
|
||||
if self.item in child_items_codes:
|
||||
_throw_error(self.item)
|
||||
|
||||
bom_nos = (
|
||||
frappe.get_all(
|
||||
"BOM Item", fields=["parent"], filters={"bom_no": self.name, "parenttype": "BOM"}
|
||||
)
|
||||
or []
|
||||
)
|
||||
|
||||
if self.name in {d.parent for d in bom_nos}:
|
||||
if self.name in {d.bom_no for d in self.items}:
|
||||
_throw_error(self.name)
|
||||
|
||||
def traverse_tree(self, bom_list=None):
|
||||
|
@ -10,7 +10,7 @@ from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import cstr, flt
|
||||
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import set_backflush_based_on
|
||||
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.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
@ -340,43 +340,36 @@ class TestBOM(FrappeTestCase):
|
||||
|
||||
def test_bom_recursion_1st_level(self):
|
||||
"""BOM should not allow BOM item again in child"""
|
||||
item_code = "_Test BOM Recursion"
|
||||
make_item(item_code, {"is_stock_item": 1})
|
||||
item_code = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom = frappe.new_doc("BOM")
|
||||
bom.item = item_code
|
||||
bom.append("items", frappe._dict(item_code=item_code))
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
bom.save()
|
||||
with self.assertRaises(BOMRecursionError):
|
||||
bom.items[0].bom_no = bom.name
|
||||
bom.save()
|
||||
|
||||
self.assertTrue("recursion" in str(err.exception).lower())
|
||||
frappe.delete_doc("BOM", bom.name, ignore_missing=True)
|
||||
|
||||
def test_bom_recursion_transitive(self):
|
||||
item1 = "_Test BOM Recursion"
|
||||
item2 = "_Test BOM Recursion 2"
|
||||
make_item(item1, {"is_stock_item": 1})
|
||||
make_item(item2, {"is_stock_item": 1})
|
||||
item1 = make_item(properties={"is_stock_item": 1}).name
|
||||
item2 = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
bom1 = frappe.new_doc("BOM")
|
||||
bom1.item = item1
|
||||
bom1.append("items", frappe._dict(item_code=item2))
|
||||
bom1.save()
|
||||
bom1.submit()
|
||||
|
||||
bom2 = frappe.new_doc("BOM")
|
||||
bom2.item = item2
|
||||
bom2.append("items", frappe._dict(item_code=item1))
|
||||
bom2.save()
|
||||
|
||||
with self.assertRaises(frappe.ValidationError) as err:
|
||||
bom2.items[0].bom_no = bom1.name
|
||||
bom1.items[0].bom_no = bom2.name
|
||||
|
||||
with self.assertRaises(BOMRecursionError):
|
||||
bom1.save()
|
||||
bom2.save()
|
||||
bom2.submit()
|
||||
|
||||
self.assertTrue("recursion" in str(err.exception).lower())
|
||||
|
||||
bom1.cancel()
|
||||
frappe.delete_doc("BOM", bom1.name, ignore_missing=True, force=True)
|
||||
frappe.delete_doc("BOM", bom2.name, ignore_missing=True, force=True)
|
||||
|
||||
def test_bom_with_process_loss_item(self):
|
||||
fg_item_non_whole, fg_item_whole, bom_item = create_process_loss_bom_items()
|
||||
|
@ -34,8 +34,7 @@ def get_data(filters):
|
||||
if filters.get(field):
|
||||
query_filters[field] = ("in", filters.get(field))
|
||||
|
||||
query_filters["report_date"] = (">=", filters.get("from_date"))
|
||||
query_filters["report_date"] = ("<=", filters.get("to_date"))
|
||||
query_filters["report_date"] = ["between", [filters.get("from_date"), filters.get("to_date")]]
|
||||
|
||||
return frappe.get_all(
|
||||
"Quality Inspection", fields=fields, filters=query_filters, order_by="report_date asc"
|
||||
|
@ -402,14 +402,15 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2022-01-13 17:40:09.474747",
|
||||
"modified": "2022-05-31 22:08:19.408223",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Manufacturing",
|
||||
"owner": "Administrator",
|
||||
"parent_page": "",
|
||||
"public": 1,
|
||||
"restrict_to_domain": "Manufacturing",
|
||||
"quick_lists": [],
|
||||
"restrict_to_domain": "",
|
||||
"roles": [],
|
||||
"sequence_id": 17.0,
|
||||
"shortcuts": [
|
||||
|
@ -231,7 +231,6 @@ erpnext.patches.v13_0.updates_for_multi_currency_payroll
|
||||
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
|
||||
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
|
||||
erpnext.patches.v13_0.update_member_email_address
|
||||
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
|
||||
erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
|
||||
erpnext.patches.v13_0.add_po_to_global_search
|
||||
erpnext.patches.v13_0.update_returned_qty_in_pr_dn
|
||||
@ -372,3 +371,5 @@ erpnext.patches.v14_0.discount_accounting_separation
|
||||
erpnext.patches.v14_0.delete_employee_transfer_property_doctype
|
||||
erpnext.patches.v13_0.create_accounting_dimensions_in_orders
|
||||
erpnext.patches.v13_0.set_per_billed_in_return_delivery_note
|
||||
execute:frappe.delete_doc("DocType", "Naming Series")
|
||||
erpnext.patches.v13_0.set_payroll_entry_status
|
||||
|
@ -1,94 +0,0 @@
|
||||
# Copyright (c) 2019, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("hr", "doctype", "leave_policy_assignment")
|
||||
frappe.reload_doc("hr", "doctype", "employee_grade")
|
||||
employee_with_assignment = []
|
||||
leave_policy = []
|
||||
|
||||
if "leave_policy" in frappe.db.get_table_columns("Employee"):
|
||||
employees_with_leave_policy = frappe.db.sql(
|
||||
"SELECT name, leave_policy FROM `tabEmployee` WHERE leave_policy IS NOT NULL and leave_policy != ''",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for employee in employees_with_leave_policy:
|
||||
alloc = frappe.db.exists(
|
||||
"Leave Allocation",
|
||||
{"employee": employee.name, "leave_policy": employee.leave_policy, "docstatus": 1},
|
||||
)
|
||||
if not alloc:
|
||||
create_assignment(employee.name, employee.leave_policy)
|
||||
|
||||
employee_with_assignment.append(employee.name)
|
||||
leave_policy.append(employee.leave_policy)
|
||||
|
||||
if "default_leave_policy" in frappe.db.get_table_columns("Employee Grade"):
|
||||
employee_grade_with_leave_policy = frappe.db.sql(
|
||||
"SELECT name, default_leave_policy FROM `tabEmployee Grade` WHERE default_leave_policy IS NOT NULL and default_leave_policy!=''",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
# for whole employee Grade
|
||||
for grade in employee_grade_with_leave_policy:
|
||||
employees = get_employee_with_grade(grade.name)
|
||||
for employee in employees:
|
||||
|
||||
if employee not in employee_with_assignment: # Will ensure no duplicate
|
||||
alloc = frappe.db.exists(
|
||||
"Leave Allocation",
|
||||
{"employee": employee.name, "leave_policy": grade.default_leave_policy, "docstatus": 1},
|
||||
)
|
||||
if not alloc:
|
||||
create_assignment(employee.name, grade.default_leave_policy)
|
||||
leave_policy.append(grade.default_leave_policy)
|
||||
|
||||
# for old Leave allocation and leave policy from allocation, which may got updated in employee grade.
|
||||
leave_allocations = frappe.db.sql(
|
||||
"SELECT leave_policy, leave_period, employee FROM `tabLeave Allocation` WHERE leave_policy IS NOT NULL and leave_policy != '' and docstatus = 1 ",
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
for allocation in leave_allocations:
|
||||
if allocation.leave_policy not in leave_policy:
|
||||
create_assignment(
|
||||
allocation.employee,
|
||||
allocation.leave_policy,
|
||||
leave_period=allocation.leave_period,
|
||||
allocation_exists=True,
|
||||
)
|
||||
|
||||
|
||||
def create_assignment(employee, leave_policy, leave_period=None, allocation_exists=False):
|
||||
if frappe.db.get_value("Leave Policy", leave_policy, "docstatus") == 2:
|
||||
return
|
||||
|
||||
filters = {"employee": employee, "leave_policy": leave_policy}
|
||||
if leave_period:
|
||||
filters["leave_period"] = leave_period
|
||||
|
||||
if not frappe.db.exists("Leave Policy Assignment", filters):
|
||||
lpa = frappe.new_doc("Leave Policy Assignment")
|
||||
lpa.employee = employee
|
||||
lpa.leave_policy = leave_policy
|
||||
|
||||
lpa.flags.ignore_mandatory = True
|
||||
if allocation_exists:
|
||||
lpa.assignment_based_on = "Leave Period"
|
||||
lpa.leave_period = leave_period
|
||||
lpa.leaves_allocated = 1
|
||||
|
||||
lpa.save()
|
||||
if allocation_exists:
|
||||
lpa.submit()
|
||||
# Updating old Leave Allocation
|
||||
frappe.db.sql("Update `tabLeave Allocation` set leave_policy_assignment = %s", lpa.name)
|
||||
|
||||
|
||||
def get_employee_with_grade(grade):
|
||||
return frappe.get_list("Employee", filters={"grade": grade})
|
@ -1,6 +1,6 @@
|
||||
import frappe
|
||||
|
||||
from erpnext.setup.doctype.naming_series.naming_series import set_by_naming_series
|
||||
from erpnext.utilities.naming import set_by_naming_series
|
||||
|
||||
|
||||
def execute():
|
||||
|
16
erpnext/patches/v13_0/set_payroll_entry_status.py
Normal file
16
erpnext/patches/v13_0/set_payroll_entry_status.py
Normal file
@ -0,0 +1,16 @@
|
||||
import frappe
|
||||
from frappe.query_builder import Case
|
||||
|
||||
|
||||
def execute():
|
||||
PayrollEntry = frappe.qb.DocType("Payroll Entry")
|
||||
|
||||
(
|
||||
frappe.qb.update(PayrollEntry).set(
|
||||
"status",
|
||||
Case()
|
||||
.when(PayrollEntry.docstatus == 0, "Draft")
|
||||
.when(PayrollEntry.docstatus == 1, "Submitted")
|
||||
.else_("Cancelled"),
|
||||
)
|
||||
).run()
|
@ -33,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document):
|
||||
self.total_declared_amount += flt(d.amount)
|
||||
|
||||
def set_total_exemption_amount(self):
|
||||
self.total_exemption_amount = get_total_exemption_amount(self.declarations)
|
||||
self.total_exemption_amount = flt(
|
||||
get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount")
|
||||
)
|
||||
|
||||
def calculate_hra_exemption(self):
|
||||
self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0
|
||||
@ -41,9 +43,18 @@ class EmployeeTaxExemptionDeclaration(Document):
|
||||
hra_exemption = calculate_annual_eligible_hra_exemption(self)
|
||||
if hra_exemption:
|
||||
self.total_exemption_amount += hra_exemption["annual_exemption"]
|
||||
self.salary_structure_hra = hra_exemption["hra_amount"]
|
||||
self.annual_hra_exemption = hra_exemption["annual_exemption"]
|
||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
||||
self.total_exemption_amount = flt(
|
||||
self.total_exemption_amount, self.precision("total_exemption_amount")
|
||||
)
|
||||
self.salary_structure_hra = flt(
|
||||
hra_exemption["hra_amount"], self.precision("salary_structure_hra")
|
||||
)
|
||||
self.annual_hra_exemption = flt(
|
||||
hra_exemption["annual_exemption"], self.precision("annual_hra_exemption")
|
||||
)
|
||||
self.monthly_hra_exemption = flt(
|
||||
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||
)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -4,25 +4,28 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months, getdate
|
||||
|
||||
import erpnext
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.utils import DuplicateDeclarationError
|
||||
|
||||
|
||||
class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
class TestEmployeeTaxExemptionDeclaration(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee@taxexepmtion.com")
|
||||
make_employee("employee1@taxexepmtion.com")
|
||||
create_payroll_period()
|
||||
make_employee("employee@taxexemption.com", company="_Test Company")
|
||||
make_employee("employee1@taxexemption.com", company="_Test Company")
|
||||
create_payroll_period(company="_Test Company")
|
||||
create_exemption_category()
|
||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""")
|
||||
frappe.db.delete("Employee Tax Exemption Declaration")
|
||||
frappe.db.delete("Salary Structure Assignment")
|
||||
|
||||
def test_duplicate_category_in_declaration(self):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -46,7 +49,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -68,7 +71,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
duplicate_declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -83,7 +86,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
)
|
||||
self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert)
|
||||
duplicate_declaration.employee = frappe.get_value(
|
||||
"Employee", {"user_id": "employee1@taxexepmtion.com"}, "name"
|
||||
"Employee", {"user_id": "employee1@taxexemption.com"}, "name"
|
||||
)
|
||||
self.assertTrue(duplicate_declaration.insert)
|
||||
|
||||
@ -91,7 +94,7 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexepmtion.com"}, "name"),
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"),
|
||||
"company": erpnext.get_default_company(),
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": erpnext.get_default_currency(),
|
||||
@ -112,6 +115,298 @@ class TestEmployeeTaxExemptionDeclaration(unittest.TestCase):
|
||||
|
||||
self.assertEqual(declaration.total_exemption_amount, 100000)
|
||||
|
||||
def test_india_hra_exemption(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Monthly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=80000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Monthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 3000)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 36000)
|
||||
# 100000 Standard Exemption + 36000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 136000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_daily_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Daily")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Daily HRA received = 3000
|
||||
# should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 17916.67)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 215000)
|
||||
# 50000 Standard Exemption + 215000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 265000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_weekly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Weekly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Weekly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 13000)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 156000)
|
||||
# 50000 Standard Exemption + 156000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 206000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_fortnightly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Fortnightly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 170000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Fortnightly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 6500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 78000)
|
||||
# 50000 Standard Exemption + 78000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 128000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_bimonthly_payroll_frequency(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
setup_hra_exemption_prerequisites("Bimonthly")
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=80000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Bimonthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 1500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 18000)
|
||||
# 100000 Standard Exemption + 18000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 118000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
def test_india_hra_exemption_with_multiple_salary_structure_assignments(self):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
make_salary_structure,
|
||||
)
|
||||
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
employee = make_employee("employee@taxexemption2.com", company="_Test Company")
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||
|
||||
create_tax_slab(
|
||||
payroll_period,
|
||||
allow_tax_exemption=True,
|
||||
currency="INR",
|
||||
effective_date=getdate("2019-04-01"),
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||
)
|
||||
|
||||
# salary structure with base 50000, HRA 3000
|
||||
make_salary_structure(
|
||||
"Monthly Structure for HRA Exemption 1",
|
||||
"Monthly",
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
from_date=payroll_period.start_date,
|
||||
)
|
||||
|
||||
# salary structure with base 70000, HRA = base * 0.2 = 14000
|
||||
salary_structure = make_salary_structure(
|
||||
"Monthly Structure for HRA Exemption 2",
|
||||
"Monthly",
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
from_date=payroll_period.start_date,
|
||||
dont_submit=True,
|
||||
)
|
||||
for component_row in salary_structure.earnings:
|
||||
if component_row.salary_component == "HRA":
|
||||
component_row.amount = 0
|
||||
component_row.amount_based_on_formula = 1
|
||||
component_row.formula = "base * 0.2"
|
||||
break
|
||||
|
||||
salary_structure.submit()
|
||||
|
||||
create_salary_structure_assignment(
|
||||
employee,
|
||||
salary_structure.name,
|
||||
from_date=add_months(payroll_period.start_date, 6),
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period.name,
|
||||
base=70000,
|
||||
allow_duplicate=True,
|
||||
)
|
||||
|
||||
declaration = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Declaration",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": payroll_period.name,
|
||||
"currency": "INR",
|
||||
"monthly_house_rent": 50000,
|
||||
"rented_in_metro_city": 1,
|
||||
"declarations": [
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
amount=60000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
# Monthly HRA received = 50000 * 6 months + 70000 * 6 months
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(declaration.monthly_hra_exemption, 8500)
|
||||
self.assertEqual(declaration.annual_hra_exemption, 102000)
|
||||
# 50000 Standard Exemption + 102000 HRA exemption
|
||||
self.assertEqual(declaration.total_exemption_amount, 152000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
||||
|
||||
def create_payroll_period(**args):
|
||||
args = frappe._dict(args)
|
||||
@ -163,3 +458,33 @@ def create_exemption_category():
|
||||
"is_active": 1,
|
||||
}
|
||||
).insert()
|
||||
|
||||
|
||||
def setup_hra_exemption_prerequisites(frequency, employee=None):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company")
|
||||
if not employee:
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name")
|
||||
|
||||
create_tax_slab(
|
||||
payroll_period,
|
||||
allow_tax_exemption=True,
|
||||
currency="INR",
|
||||
effective_date=getdate("2019-04-01"),
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
make_salary_structure(
|
||||
f"{frequency} Structure for HRA Exemption",
|
||||
frequency,
|
||||
employee=employee,
|
||||
company="_Test Company",
|
||||
currency="INR",
|
||||
payroll_period=payroll_period,
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"}
|
||||
)
|
||||
|
@ -31,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
||||
self.total_actual_amount += flt(d.amount)
|
||||
|
||||
def set_total_exemption_amount(self):
|
||||
self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs)
|
||||
self.exemption_amount = flt(
|
||||
get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount")
|
||||
)
|
||||
|
||||
def calculate_hra_exemption(self):
|
||||
self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0
|
||||
@ -39,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document):
|
||||
hra_exemption = calculate_hra_exemption_for_period(self)
|
||||
if hra_exemption:
|
||||
self.exemption_amount += hra_exemption["total_eligible_hra_exemption"]
|
||||
self.monthly_hra_exemption = hra_exemption["monthly_exemption"]
|
||||
self.monthly_house_rent = hra_exemption["monthly_house_rent"]
|
||||
self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"]
|
||||
self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount"))
|
||||
self.monthly_hra_exemption = flt(
|
||||
hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption")
|
||||
)
|
||||
self.monthly_house_rent = flt(
|
||||
hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent")
|
||||
)
|
||||
self.total_eligible_hra_exemption = flt(
|
||||
hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption")
|
||||
)
|
||||
|
@ -4,22 +4,26 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import (
|
||||
create_exemption_category,
|
||||
create_payroll_period,
|
||||
setup_hra_exemption_prerequisites,
|
||||
)
|
||||
|
||||
|
||||
class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
def setup(self):
|
||||
make_employee("employee@proofsubmission.com")
|
||||
create_payroll_period()
|
||||
class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase):
|
||||
def setUp(self):
|
||||
make_employee("employee@proofsubmission.com", company="_Test Company")
|
||||
create_payroll_period(company="_Test Company")
|
||||
create_exemption_category()
|
||||
frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""")
|
||||
frappe.db.delete("Employee Tax Exemption Proof Submission")
|
||||
frappe.db.delete("Salary Structure Assignment")
|
||||
|
||||
def test_exemption_amount_lesser_than_category_max(self):
|
||||
declaration = frappe.get_doc(
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||
@ -34,8 +38,8 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
||||
declaration = frappe.get_doc(
|
||||
self.assertRaises(frappe.ValidationError, proof.save)
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"payroll_period": "Test Payroll Period",
|
||||
@ -50,11 +54,11 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertTrue(declaration.save)
|
||||
self.assertTrue(declaration.submit)
|
||||
self.assertTrue(proof.save)
|
||||
self.assertTrue(proof.submit)
|
||||
|
||||
def test_duplicate_category_in_proof_submission(self):
|
||||
declaration = frappe.get_doc(
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"),
|
||||
@ -74,4 +78,59 @@ class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase):
|
||||
],
|
||||
}
|
||||
)
|
||||
self.assertRaises(frappe.ValidationError, declaration.save)
|
||||
self.assertRaises(frappe.ValidationError, proof.save)
|
||||
|
||||
def test_india_hra_exemption(self):
|
||||
# set country
|
||||
current_country = frappe.flags.country
|
||||
frappe.flags.country = "India"
|
||||
|
||||
employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name")
|
||||
setup_hra_exemption_prerequisites("Monthly", employee)
|
||||
payroll_period = frappe.db.get_value(
|
||||
"Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True
|
||||
)
|
||||
|
||||
proof = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Employee Tax Exemption Proof Submission",
|
||||
"employee": employee,
|
||||
"company": "_Test Company",
|
||||
"payroll_period": "_Test Payroll Period",
|
||||
"currency": "INR",
|
||||
"house_rent_payment_amount": 600000,
|
||||
"rented_in_metro_city": 1,
|
||||
"rented_from_date": payroll_period.start_date,
|
||||
"rented_to_date": payroll_period.end_date,
|
||||
"tax_exemption_proofs": [
|
||||
dict(
|
||||
exemption_sub_category="_Test Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
type_of_proof="Test Proof",
|
||||
amount=100000,
|
||||
),
|
||||
dict(
|
||||
exemption_sub_category="_Test1 Sub Category",
|
||||
exemption_category="_Test Category",
|
||||
type_of_proof="Test Proof",
|
||||
amount=50000,
|
||||
),
|
||||
],
|
||||
}
|
||||
).insert()
|
||||
|
||||
self.assertEqual(proof.monthly_house_rent, 50000)
|
||||
|
||||
# Monthly HRA received = 3000
|
||||
# should set HRA exemption as per actual annual HRA because that's the minimum
|
||||
self.assertEqual(proof.monthly_hra_exemption, 3000)
|
||||
self.assertEqual(proof.total_eligible_hra_exemption, 36000)
|
||||
|
||||
# total exemptions + house rent payment amount
|
||||
self.assertEqual(proof.total_actual_amount, 750000)
|
||||
|
||||
# 100000 Standard Exemption + 36000 HRA exemption
|
||||
self.assertEqual(proof.exemption_amount, 136000)
|
||||
|
||||
# reset
|
||||
frappe.flags.country = current_country
|
||||
|
@ -76,9 +76,8 @@
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"options": "Draft\nUnpaid\nPaid",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
"options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: !doc.pay_via_salary_slip",
|
||||
@ -194,7 +193,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-02-02 14:00:45.536152",
|
||||
"modified": "2022-05-27 13:56:14.349183",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Gratuity",
|
||||
|
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
12
erpnext/payroll/doctype/gratuity/gratuity_list.js
Normal file
@ -0,0 +1,12 @@
|
||||
frappe.listview_settings["Gratuity"] = {
|
||||
get_indicator: function(doc) {
|
||||
let status_color = {
|
||||
"Draft": "red",
|
||||
"Submitted": "blue",
|
||||
"Cancelled": "red",
|
||||
"Paid": "green",
|
||||
"Unpaid": "orange",
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||
}
|
||||
};
|
@ -4,57 +4,69 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import add_days, flt, get_datetime, getdate
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate
|
||||
|
||||
from erpnext.hr.doctype.employee.test_employee import make_employee
|
||||
from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
|
||||
from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list
|
||||
from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
make_deduction_salary_component,
|
||||
make_earning_salary_component,
|
||||
make_employee_salary_slip,
|
||||
make_holiday_list,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
|
||||
|
||||
test_dependencies = ["Salary Component", "Salary Slip", "Account"]
|
||||
|
||||
|
||||
class TestGratuity(unittest.TestCase):
|
||||
class TestGratuity(FrappeTestCase):
|
||||
def setUp(self):
|
||||
frappe.db.delete("Gratuity")
|
||||
frappe.db.delete("Salary Slip")
|
||||
frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"})
|
||||
|
||||
make_earning_salary_component(
|
||||
setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True
|
||||
)
|
||||
make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"])
|
||||
make_holiday_list()
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_get_last_salary_slip_should_return_none_for_new_employee(self):
|
||||
new_employee = make_employee("new_employee@salary.com", company="_Test Company")
|
||||
salary_slip = get_last_salary_slip(new_employee)
|
||||
assert salary_slip is None
|
||||
self.assertIsNone(salary_slip)
|
||||
|
||||
def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_gratuity_based_on_current_slab_via_additional_salary(self):
|
||||
"""
|
||||
Range | Fraction
|
||||
5-0 | 1
|
||||
"""
|
||||
doj = add_days(getdate(), -(6 * 365))
|
||||
relieving_date = getdate()
|
||||
|
||||
employee = make_employee(
|
||||
"test_employee_gratuity@salary.com",
|
||||
company="_Test Company",
|
||||
date_of_joining=doj,
|
||||
relieving_date=relieving_date,
|
||||
)
|
||||
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||
|
||||
rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
|
||||
gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name)
|
||||
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
employee_total_workings_days = (
|
||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
||||
).days
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||
self.assertEqual(gratuity.current_work_experience, experience)
|
||||
|
||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
||||
gratuity.reload()
|
||||
from math import floor
|
||||
|
||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
||||
|
||||
# amount Calculation
|
||||
# amount calculation
|
||||
component_amount = frappe.get_all(
|
||||
"Salary Detail",
|
||||
filters={
|
||||
@ -64,20 +76,44 @@ class TestGratuity(unittest.TestCase):
|
||||
"salary_component": "Basic Salary",
|
||||
},
|
||||
fields=["amount"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
""" 5 - 0 fraction is 1 """
|
||||
|
||||
gratuity_amount = component_amount[0].amount * experience
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
# additional salary creation (Pay via salary slip)
|
||||
self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name}))
|
||||
|
||||
def test_check_gratuity_amount_based_on_all_previous_slabs(self):
|
||||
employee, sal_slip = create_employee_and_get_last_salary_slip()
|
||||
# gratuity should be marked "Paid" on the next salary slip submission
|
||||
salary_slip = make_salary_slip("Test Gratuity", employee=employee)
|
||||
salary_slip.posting_date = getdate()
|
||||
salary_slip.insert()
|
||||
salary_slip.submit()
|
||||
|
||||
gratuity.reload()
|
||||
self.assertEqual(gratuity.status, "Paid")
|
||||
|
||||
@set_holiday_list("Salary Slip Test Holiday List", "_Test Company")
|
||||
def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self):
|
||||
"""
|
||||
Range | Fraction
|
||||
0-1 | 0
|
||||
1-5 | 0.7
|
||||
5-0 | 1
|
||||
"""
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
|
||||
doj = add_days(getdate(), -(6 * 365))
|
||||
relieving_date = getdate()
|
||||
|
||||
employee = make_employee(
|
||||
"test_employee_gratuity@salary.com",
|
||||
company="_Test Company",
|
||||
date_of_joining=doj,
|
||||
relieving_date=relieving_date,
|
||||
)
|
||||
|
||||
sal_slip = create_salary_slip("test_employee_gratuity@salary.com")
|
||||
rule = get_gratuity_rule("Rule Under Limited Contract (UAE)")
|
||||
set_mode_of_payment_account()
|
||||
|
||||
@ -86,22 +122,11 @@ class TestGratuity(unittest.TestCase):
|
||||
)
|
||||
|
||||
# work experience calculation
|
||||
date_of_joining, relieving_date = frappe.db.get_value(
|
||||
"Employee", employee, ["date_of_joining", "relieving_date"]
|
||||
)
|
||||
employee_total_workings_days = (
|
||||
get_datetime(relieving_date) - get_datetime(date_of_joining)
|
||||
).days
|
||||
employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days
|
||||
experience = floor(employee_total_workings_days / rule.total_working_days_per_year)
|
||||
self.assertEqual(gratuity.current_work_experience, experience)
|
||||
|
||||
experience = employee_total_workings_days / rule.total_working_days_per_year
|
||||
|
||||
gratuity.reload()
|
||||
|
||||
from math import floor
|
||||
|
||||
self.assertEqual(floor(experience), gratuity.current_work_experience)
|
||||
|
||||
# amount Calculation
|
||||
# amount calculation
|
||||
component_amount = frappe.get_all(
|
||||
"Salary Detail",
|
||||
filters={
|
||||
@ -111,35 +136,22 @@ class TestGratuity(unittest.TestCase):
|
||||
"salary_component": "Basic Salary",
|
||||
},
|
||||
fields=["amount"],
|
||||
limit=1,
|
||||
)
|
||||
|
||||
""" range | Fraction
|
||||
0-1 | 0
|
||||
1-5 | 0.7
|
||||
5-0 | 1
|
||||
"""
|
||||
|
||||
gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2))
|
||||
self.assertEqual(gratuity.status, "Unpaid")
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
pe = get_payment_entry("Gratuity", gratuity.name)
|
||||
pe.reference_no = "123467"
|
||||
pe.reference_date = getdate()
|
||||
pe.submit()
|
||||
|
||||
pay_entry = get_payment_entry("Gratuity", gratuity.name)
|
||||
pay_entry.reference_no = "123467"
|
||||
pay_entry.reference_date = getdate()
|
||||
pay_entry.save()
|
||||
pay_entry.submit()
|
||||
gratuity.reload()
|
||||
|
||||
self.assertEqual(gratuity.status, "Paid")
|
||||
self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2))
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
||||
|
||||
|
||||
def get_gratuity_rule(name):
|
||||
rule = frappe.db.exists("Gratuity Rule", name)
|
||||
@ -149,7 +161,6 @@ def get_gratuity_rule(name):
|
||||
rule.applicable_earnings_component = []
|
||||
rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"})
|
||||
rule.save()
|
||||
rule.reload()
|
||||
|
||||
return rule
|
||||
|
||||
@ -204,23 +215,17 @@ def create_account():
|
||||
).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def create_employee_and_get_last_salary_slip():
|
||||
employee = make_employee("test_employee@salary.com", company="_Test Company")
|
||||
frappe.db.set_value("Employee", employee, "relieving_date", getdate())
|
||||
frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), -(6 * 365)))
|
||||
def create_salary_slip(employee):
|
||||
if not frappe.db.exists("Salary Slip", {"employee": employee}):
|
||||
salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly")
|
||||
posting_date = get_first_day(add_months(getdate(), -1))
|
||||
salary_slip = make_employee_salary_slip(
|
||||
employee, "Monthly", "Test Gratuity", posting_date=posting_date
|
||||
)
|
||||
salary_slip.start_date = posting_date
|
||||
salary_slip.end_date = None
|
||||
salary_slip.submit()
|
||||
salary_slip = salary_slip.name
|
||||
else:
|
||||
salary_slip = get_last_salary_slip(employee)
|
||||
|
||||
if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"):
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list
|
||||
|
||||
make_holiday_list()
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_holiday_list", "Salary Slip Test Holiday List"
|
||||
)
|
||||
|
||||
return employee, salary_slip
|
||||
return salary_slip
|
||||
|
@ -40,30 +40,69 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
},
|
||||
|
||||
refresh: function (frm) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
if (!frm.is_new()) {
|
||||
if (frm.doc.docstatus === 0 && !frm.is_new()) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.add_custom_button(__("Get Employees"),
|
||||
function () {
|
||||
frm.events.get_employee_details(frm);
|
||||
}
|
||||
).toggleClass("btn-primary", !(frm.doc.employees || []).length);
|
||||
}
|
||||
|
||||
if (
|
||||
(frm.doc.employees || []).length
|
||||
&& !frappe.model.has_workflow(frm.doctype)
|
||||
&& !cint(frm.doc.salary_slips_created)
|
||||
&& (frm.doc.docstatus != 2)
|
||||
) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.add_custom_button(__("Get Employees"),
|
||||
function () {
|
||||
frm.events.get_employee_details(frm);
|
||||
}
|
||||
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
|
||||
}
|
||||
if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
|
||||
frm.page.clear_primary_action();
|
||||
frm.page.set_primary_action(__('Create Salary Slips'), () => {
|
||||
frm.save('Submit').then(() => {
|
||||
frm.page.set_primary_action(__("Create Salary Slips"), () => {
|
||||
frm.save("Submit").then(() => {
|
||||
frm.page.clear_primary_action();
|
||||
frm.refresh();
|
||||
frm.events.refresh(frm);
|
||||
});
|
||||
});
|
||||
} else if (frm.doc.docstatus == 1 && frm.doc.status == "Failed") {
|
||||
frm.add_custom_button(__("Create Salary Slip"), function () {
|
||||
frm.call("create_salary_slips", {}, () => {
|
||||
frm.reload_doc();
|
||||
});
|
||||
}).addClass("btn-primary");
|
||||
}
|
||||
}
|
||||
if (frm.doc.docstatus == 1) {
|
||||
|
||||
if (frm.doc.docstatus == 1 && frm.doc.status == "Submitted") {
|
||||
if (frm.custom_buttons) frm.clear_custom_buttons();
|
||||
frm.events.add_context_buttons(frm);
|
||||
}
|
||||
|
||||
if (frm.doc.status == "Failed" && frm.doc.error_message) {
|
||||
const issue = `<a id="jump_to_error" style="text-decoration: underline;">issue</a>`;
|
||||
let process = (cint(frm.doc.salary_slips_created)) ? "submission" : "creation";
|
||||
|
||||
frm.dashboard.set_headline(
|
||||
__("Salary Slip {0} failed. You can resolve the {1} and retry {0}.", [process, issue])
|
||||
);
|
||||
|
||||
$("#jump_to_error").on("click", (e) => {
|
||||
e.preventDefault();
|
||||
frappe.utils.scroll_to(
|
||||
frm.get_field("error_message").$wrapper,
|
||||
true,
|
||||
30
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
frappe.realtime.on("completed_salary_slip_creation", function() {
|
||||
frm.reload_doc();
|
||||
});
|
||||
|
||||
frappe.realtime.on("completed_salary_slip_submission", function() {
|
||||
frm.reload_doc();
|
||||
});
|
||||
},
|
||||
|
||||
get_employee_details: function (frm) {
|
||||
@ -88,7 +127,7 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
doc: frm.doc,
|
||||
method: "create_salary_slips",
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
frm.reload_doc();
|
||||
frm.toolbar.refresh();
|
||||
}
|
||||
});
|
||||
@ -97,7 +136,7 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
add_context_buttons: function (frm) {
|
||||
if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
|
||||
frm.events.add_bank_entry_button(frm);
|
||||
} else if (frm.doc.salary_slips_created) {
|
||||
} else if (frm.doc.salary_slips_created && frm.doc.status != 'Queued') {
|
||||
frm.add_custom_button(__("Submit Salary Slip"), function () {
|
||||
submit_salary_slip(frm);
|
||||
}).addClass("btn-primary");
|
||||
@ -331,6 +370,7 @@ const submit_salary_slip = function (frm) {
|
||||
method: 'submit_salary_slips',
|
||||
args: {},
|
||||
callback: function () {
|
||||
frm.reload_doc();
|
||||
frm.events.refresh(frm);
|
||||
},
|
||||
doc: frm.doc,
|
||||
|
@ -8,11 +8,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"section_break0",
|
||||
"column_break0",
|
||||
"posting_date",
|
||||
"payroll_frequency",
|
||||
"company",
|
||||
"column_break1",
|
||||
"status",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"payroll_payable_account",
|
||||
@ -41,11 +41,14 @@
|
||||
"cost_center",
|
||||
"account",
|
||||
"payment_account",
|
||||
"amended_from",
|
||||
"column_break_33",
|
||||
"bank_account",
|
||||
"salary_slips_created",
|
||||
"salary_slips_submitted"
|
||||
"salary_slips_submitted",
|
||||
"failure_details_section",
|
||||
"error_message",
|
||||
"section_break_41",
|
||||
"amended_from"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -53,11 +56,6 @@
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Select Employees"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break0",
|
||||
"fieldtype": "Column Break",
|
||||
"width": "50%"
|
||||
},
|
||||
{
|
||||
"default": "Today",
|
||||
"fieldname": "posting_date",
|
||||
@ -231,6 +229,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Salary Slips Created",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -239,6 +238,7 @@
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Salary Slips Submitted",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -284,15 +284,44 @@
|
||||
"label": "Payroll Payable Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "error_message",
|
||||
"depends_on": "eval:doc.status=='Failed';",
|
||||
"fieldname": "failure_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Failure Details"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.status=='Failed';",
|
||||
"fieldname": "error_message",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Error Message",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_41",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Draft\nSubmitted\nCancelled\nQueued\nFailed",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-cog",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-17 15:13:17.766210",
|
||||
"modified": "2022-03-16 12:45:21.662765",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Payroll",
|
||||
"name": "Payroll Entry",
|
||||
"naming_rule": "Expression (old style)",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
@ -308,5 +337,6 @@
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
"sort_order": "DESC",
|
||||
"states": []
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import json
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
@ -40,8 +41,10 @@ class PayrollEntry(Document):
|
||||
|
||||
def validate(self):
|
||||
self.number_of_employees = len(self.employees)
|
||||
self.set_status()
|
||||
|
||||
def on_submit(self):
|
||||
self.set_status(update=True, status="Submitted")
|
||||
self.create_salary_slips()
|
||||
|
||||
def before_submit(self):
|
||||
@ -51,6 +54,15 @@ class PayrollEntry(Document):
|
||||
if self.validate_employee_attendance():
|
||||
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
|
||||
|
||||
def set_status(self, status=None, update=False):
|
||||
if not status:
|
||||
status = {0: "Draft", 1: "Submitted", 2: "Cancelled"}[self.docstatus or 0]
|
||||
|
||||
if update:
|
||||
self.db_set("status", status)
|
||||
else:
|
||||
self.status = status
|
||||
|
||||
def validate_employee_details(self):
|
||||
emp_with_sal_slip = []
|
||||
for employee_details in self.employees:
|
||||
@ -87,6 +99,8 @@ class PayrollEntry(Document):
|
||||
)
|
||||
self.db_set("salary_slips_created", 0)
|
||||
self.db_set("salary_slips_submitted", 0)
|
||||
self.set_status(update=True, status="Cancelled")
|
||||
self.db_set("error_message", "")
|
||||
|
||||
def get_emp_list(self):
|
||||
"""
|
||||
@ -183,8 +197,20 @@ class PayrollEntry(Document):
|
||||
"currency": self.currency,
|
||||
}
|
||||
)
|
||||
if len(employees) > 30:
|
||||
frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
|
||||
if len(employees) > 30 or frappe.flags.enqueue_payroll_entry:
|
||||
self.db_set("status", "Queued")
|
||||
frappe.enqueue(
|
||||
create_salary_slips_for_employees,
|
||||
timeout=600,
|
||||
employees=employees,
|
||||
args=args,
|
||||
publish_progress=False,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Salary Slip creation is queued. It may take a few minutes"),
|
||||
alert=True,
|
||||
indicator="blue",
|
||||
)
|
||||
else:
|
||||
create_salary_slips_for_employees(employees, args, publish_progress=False)
|
||||
# since this method is called via frm.call this doc needs to be updated manually
|
||||
@ -214,13 +240,23 @@ class PayrollEntry(Document):
|
||||
@frappe.whitelist()
|
||||
def submit_salary_slips(self):
|
||||
self.check_permission("write")
|
||||
ss_list = self.get_sal_slip_list(ss_status=0)
|
||||
if len(ss_list) > 30:
|
||||
salary_slips = self.get_sal_slip_list(ss_status=0)
|
||||
if len(salary_slips) > 30 or frappe.flags.enqueue_payroll_entry:
|
||||
self.db_set("status", "Queued")
|
||||
frappe.enqueue(
|
||||
submit_salary_slips_for_employees, timeout=600, payroll_entry=self, salary_slips=ss_list
|
||||
submit_salary_slips_for_employees,
|
||||
timeout=600,
|
||||
payroll_entry=self,
|
||||
salary_slips=salary_slips,
|
||||
publish_progress=False,
|
||||
)
|
||||
frappe.msgprint(
|
||||
_("Salary Slip submission is queued. It may take a few minutes"),
|
||||
alert=True,
|
||||
indicator="blue",
|
||||
)
|
||||
else:
|
||||
submit_salary_slips_for_employees(self, ss_list, publish_progress=False)
|
||||
submit_salary_slips_for_employees(self, salary_slips, publish_progress=False)
|
||||
|
||||
def email_salary_slip(self, submitted_ss):
|
||||
if frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee"):
|
||||
@ -233,7 +269,11 @@ class PayrollEntry(Document):
|
||||
)
|
||||
|
||||
if not account:
|
||||
frappe.throw(_("Please set account in Salary Component {0}").format(salary_component))
|
||||
frappe.throw(
|
||||
_("Please set account in Salary Component {0}").format(
|
||||
get_link_to_form("Salary Component", salary_component)
|
||||
)
|
||||
)
|
||||
|
||||
return account
|
||||
|
||||
@ -790,36 +830,80 @@ def payroll_entry_has_bank_entries(name):
|
||||
return response
|
||||
|
||||
|
||||
def log_payroll_failure(process, payroll_entry, error):
|
||||
error_log = frappe.log_error(
|
||||
title=_("Salary Slip {0} failed for Payroll Entry {1}").format(process, payroll_entry.name)
|
||||
)
|
||||
message_log = frappe.message_log.pop() if frappe.message_log else str(error)
|
||||
|
||||
try:
|
||||
error_message = json.loads(message_log).get("message")
|
||||
except Exception:
|
||||
error_message = message_log
|
||||
|
||||
error_message += "\n" + _("Check Error Log {0} for more details.").format(
|
||||
get_link_to_form("Error Log", error_log.name)
|
||||
)
|
||||
|
||||
payroll_entry.db_set({"error_message": error_message, "status": "Failed"})
|
||||
|
||||
|
||||
def create_salary_slips_for_employees(employees, args, publish_progress=True):
|
||||
salary_slips_exists_for = get_existing_salary_slips(employees, args)
|
||||
count = 0
|
||||
salary_slips_not_created = []
|
||||
for emp in employees:
|
||||
if emp not in salary_slips_exists_for:
|
||||
args.update({"doctype": "Salary Slip", "employee": emp})
|
||||
ss = frappe.get_doc(args)
|
||||
ss.insert()
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(
|
||||
count * 100 / len(set(employees) - set(salary_slips_exists_for)),
|
||||
title=_("Creating Salary Slips..."),
|
||||
)
|
||||
try:
|
||||
payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
|
||||
salary_slips_exist_for = get_existing_salary_slips(employees, args)
|
||||
count = 0
|
||||
|
||||
else:
|
||||
salary_slips_not_created.append(emp)
|
||||
for emp in employees:
|
||||
if emp not in salary_slips_exist_for:
|
||||
args.update({"doctype": "Salary Slip", "employee": emp})
|
||||
frappe.get_doc(args).insert()
|
||||
|
||||
payroll_entry = frappe.get_doc("Payroll Entry", args.payroll_entry)
|
||||
payroll_entry.db_set("salary_slips_created", 1)
|
||||
payroll_entry.notify_update()
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(
|
||||
count * 100 / len(set(employees) - set(salary_slips_exist_for)),
|
||||
title=_("Creating Salary Slips..."),
|
||||
)
|
||||
|
||||
if salary_slips_not_created:
|
||||
payroll_entry.db_set({"status": "Submitted", "salary_slips_created": 1, "error_message": ""})
|
||||
|
||||
if salary_slips_exist_for:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Salary Slips already exist for employees {}, and will not be processed by this payroll."
|
||||
).format(frappe.bold(", ".join(emp for emp in salary_slips_exist_for))),
|
||||
title=_("Message"),
|
||||
indicator="orange",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
log_payroll_failure("creation", payroll_entry, e)
|
||||
|
||||
finally:
|
||||
frappe.db.commit() # nosemgrep
|
||||
frappe.publish_realtime("completed_salary_slip_creation")
|
||||
|
||||
|
||||
def show_payroll_submission_status(submitted, unsubmitted, payroll_entry):
|
||||
if not submitted and not unsubmitted:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"Salary Slips already exists for employees {}, and will not be processed by this payroll."
|
||||
).format(frappe.bold(", ".join([emp for emp in salary_slips_not_created]))),
|
||||
title=_("Message"),
|
||||
indicator="orange",
|
||||
"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
|
||||
)
|
||||
)
|
||||
elif submitted and not unsubmitted:
|
||||
frappe.msgprint(
|
||||
_("Salary Slips submitted for period from {0} to {1}").format(
|
||||
payroll_entry.start_date, payroll_entry.end_date
|
||||
)
|
||||
)
|
||||
elif unsubmitted:
|
||||
frappe.msgprint(
|
||||
_("Could not submit some Salary Slips: {}").format(
|
||||
", ".join(get_link_to_form("Salary Slip", entry) for entry in unsubmitted)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@ -837,45 +921,41 @@ def get_existing_salary_slips(employees, args):
|
||||
|
||||
|
||||
def submit_salary_slips_for_employees(payroll_entry, salary_slips, publish_progress=True):
|
||||
submitted_ss = []
|
||||
not_submitted_ss = []
|
||||
frappe.flags.via_payroll_entry = True
|
||||
try:
|
||||
submitted = []
|
||||
unsubmitted = []
|
||||
frappe.flags.via_payroll_entry = True
|
||||
count = 0
|
||||
|
||||
count = 0
|
||||
for ss in salary_slips:
|
||||
ss_obj = frappe.get_doc("Salary Slip", ss[0])
|
||||
if ss_obj.net_pay < 0:
|
||||
not_submitted_ss.append(ss[0])
|
||||
else:
|
||||
try:
|
||||
ss_obj.submit()
|
||||
submitted_ss.append(ss_obj)
|
||||
except frappe.ValidationError:
|
||||
not_submitted_ss.append(ss[0])
|
||||
for entry in salary_slips:
|
||||
salary_slip = frappe.get_doc("Salary Slip", entry[0])
|
||||
if salary_slip.net_pay < 0:
|
||||
unsubmitted.append(entry[0])
|
||||
else:
|
||||
try:
|
||||
salary_slip.submit()
|
||||
submitted.append(salary_slip)
|
||||
except frappe.ValidationError:
|
||||
unsubmitted.append(entry[0])
|
||||
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
|
||||
if submitted_ss:
|
||||
payroll_entry.make_accrual_jv_entry()
|
||||
frappe.msgprint(
|
||||
_("Salary Slip submitted for period from {0} to {1}").format(ss_obj.start_date, ss_obj.end_date)
|
||||
)
|
||||
count += 1
|
||||
if publish_progress:
|
||||
frappe.publish_progress(count * 100 / len(salary_slips), title=_("Submitting Salary Slips..."))
|
||||
|
||||
payroll_entry.email_salary_slip(submitted_ss)
|
||||
if submitted:
|
||||
payroll_entry.make_accrual_jv_entry()
|
||||
payroll_entry.email_salary_slip(submitted)
|
||||
payroll_entry.db_set({"salary_slips_submitted": 1, "status": "Submitted", "error_message": ""})
|
||||
|
||||
payroll_entry.db_set("salary_slips_submitted", 1)
|
||||
payroll_entry.notify_update()
|
||||
show_payroll_submission_status(submitted, unsubmitted, payroll_entry)
|
||||
|
||||
if not submitted_ss and not not_submitted_ss:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"No salary slip found to submit for the above selected criteria OR salary slip already submitted"
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
log_payroll_failure("submission", payroll_entry, e)
|
||||
|
||||
if not_submitted_ss:
|
||||
frappe.msgprint(_("Could not submit some Salary Slips"))
|
||||
finally:
|
||||
frappe.db.commit() # nosemgrep
|
||||
frappe.publish_realtime("completed_salary_slip_submission")
|
||||
|
||||
frappe.flags.via_payroll_entry = False
|
||||
|
||||
|
18
erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
Normal file
18
erpnext/payroll/doctype/payroll_entry/payroll_entry_list.js
Normal file
@ -0,0 +1,18 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings['Payroll Entry'] = {
|
||||
has_indicator_for_draft: 1,
|
||||
get_indicator: function(doc) {
|
||||
var status_color = {
|
||||
'Draft': 'red',
|
||||
'Submitted': 'blue',
|
||||
'Queued': 'orange',
|
||||
'Failed': 'red',
|
||||
'Cancelled': 'red'
|
||||
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
|
||||
}
|
||||
};
|
@ -5,6 +5,7 @@ import unittest
|
||||
|
||||
import frappe
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from frappe.tests.utils import FrappeTestCase
|
||||
from frappe.utils import add_months
|
||||
|
||||
import erpnext
|
||||
@ -22,10 +23,9 @@ from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_
|
||||
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_end_date, get_start_end_dates
|
||||
from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
|
||||
create_account,
|
||||
get_salary_component_account,
|
||||
make_deduction_salary_component,
|
||||
make_earning_salary_component,
|
||||
make_employee_salary_slip,
|
||||
set_salary_component_account,
|
||||
)
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
create_salary_structure_assignment,
|
||||
@ -35,13 +35,7 @@ from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||
test_dependencies = ["Holiday List"]
|
||||
|
||||
|
||||
class TestPayrollEntry(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls):
|
||||
frappe.db.set_value(
|
||||
"Company", erpnext.get_default_company(), "default_holiday_list", "_Test Holiday List"
|
||||
)
|
||||
|
||||
class TestPayrollEntry(FrappeTestCase):
|
||||
def setUp(self):
|
||||
for dt in [
|
||||
"Salary Slip",
|
||||
@ -52,81 +46,72 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
"Salary Structure Assignment",
|
||||
"Payroll Employee Detail",
|
||||
"Additional Salary",
|
||||
"Loan",
|
||||
]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
frappe.db.delete(dt)
|
||||
|
||||
make_earning_salary_component(setup=True, company_list=["_Test Company"])
|
||||
make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
|
||||
|
||||
frappe.db.set_value("Company", "_Test Company", "default_holiday_list", "_Test Holiday List")
|
||||
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
|
||||
|
||||
def test_payroll_entry(self): # pylint: disable=no-self-use
|
||||
company = erpnext.get_default_company()
|
||||
for data in frappe.get_all("Salary Component", fields=["name"]):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data.name, "company": company}, "name"
|
||||
):
|
||||
get_salary_component_account(data.name)
|
||||
|
||||
employee = frappe.db.get_value("Employee", {"company": company})
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
make_salary_structure(
|
||||
"_Test Salary Structure",
|
||||
"Monthly",
|
||||
employee,
|
||||
company=company,
|
||||
currency=company_doc.default_currency,
|
||||
# set default payable account
|
||||
default_account = frappe.db.get_value(
|
||||
"Company", "_Test Company", "default_payroll_payable_account"
|
||||
)
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
if not frappe.db.get_value(
|
||||
"Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
|
||||
):
|
||||
make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
if not default_account or default_account != "_Test Payroll Payable - _TC":
|
||||
create_account(
|
||||
account_name="_Test Payroll Payable",
|
||||
company="_Test Company",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_type="Payable",
|
||||
)
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
|
||||
)
|
||||
|
||||
def test_multi_currency_payroll_entry(self): # pylint: disable=no-self-use
|
||||
company = erpnext.get_default_company()
|
||||
employee = make_employee("test_muti_currency_employee@payroll.com", company=company)
|
||||
for data in frappe.get_all("Salary Component", fields=["name"]):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data.name, "company": company}, "name"
|
||||
):
|
||||
get_salary_component_account(data.name)
|
||||
def test_payroll_entry(self):
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
employee = frappe.db.get_value("Employee", {"company": "_Test Company"})
|
||||
setup_salary_structure(employee, company)
|
||||
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
salary_structure = make_salary_structure(
|
||||
"_Test Multi Currency Salary Structure", "Monthly", company=company, currency="USD"
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company.default_payroll_payable_account,
|
||||
currency=company.default_currency,
|
||||
company=company.name,
|
||||
)
|
||||
create_salary_structure_assignment(
|
||||
employee, salary_structure.name, company=company, currency="USD"
|
||||
)
|
||||
frappe.db.sql(
|
||||
"""delete from `tabSalary Slip` where employee=%s""",
|
||||
(frappe.db.get_value("Employee", {"user_id": "test_muti_currency_employee@payroll.com"})),
|
||||
)
|
||||
salary_slip = get_salary_slip(
|
||||
"test_muti_currency_employee@payroll.com", "Monthly", "_Test Multi Currency Salary Structure"
|
||||
|
||||
def test_multi_currency_payroll_entry(self):
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
employee = make_employee(
|
||||
"test_muti_currency_employee@payroll.com", company=company.name, department="Accounts - _TC"
|
||||
)
|
||||
salary_structure = "_Test Multi Currency Salary Structure"
|
||||
setup_salary_structure(employee, company, "USD", salary_structure)
|
||||
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
payable_account=company.default_payroll_payable_account,
|
||||
currency="USD",
|
||||
exchange_rate=70,
|
||||
company=company.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
payroll_entry.make_payment_entry()
|
||||
|
||||
salary_slip.load_from_db()
|
||||
salary_slip = frappe.db.get_value("Salary Slip", {"payroll_entry": payroll_entry.name}, "name")
|
||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip)
|
||||
|
||||
payroll_entry.reload()
|
||||
payroll_je = salary_slip.journal_entry
|
||||
if payroll_je:
|
||||
payroll_je_doc = frappe.get_doc("Journal Entry", payroll_je)
|
||||
|
||||
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_debit)
|
||||
self.assertEqual(salary_slip.base_gross_pay, payroll_je_doc.total_credit)
|
||||
|
||||
@ -139,27 +124,15 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
(payroll_entry.name),
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_debit)
|
||||
self.assertEqual(salary_slip.base_net_pay, payment_entry[0].total_credit)
|
||||
|
||||
def test_payroll_entry_with_employee_cost_center(self): # pylint: disable=no-self-use
|
||||
for data in frappe.get_all("Salary Component", fields=["name"]):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data.name, "company": "_Test Company"}, "name"
|
||||
):
|
||||
get_salary_component_account(data.name)
|
||||
|
||||
def test_payroll_entry_with_employee_cost_center(self):
|
||||
if not frappe.db.exists("Department", "cc - _TC"):
|
||||
frappe.get_doc(
|
||||
{"doctype": "Department", "department_name": "cc", "company": "_Test Company"}
|
||||
).insert()
|
||||
|
||||
frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee1@example.com' """)
|
||||
frappe.db.sql("""delete from `tabEmployee` where employee_name='test_employee2@example.com' """)
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 1' """)
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name='_Test Salary Structure 2' """)
|
||||
|
||||
employee1 = make_employee(
|
||||
"test_employee1@example.com",
|
||||
payroll_cost_center="_Test Cost Center - _TC",
|
||||
@ -170,38 +143,15 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
"test_employee2@example.com", department="cc - _TC", company="_Test Company"
|
||||
)
|
||||
|
||||
if not frappe.db.exists("Account", "_Test Payroll Payable - _TC"):
|
||||
create_account(
|
||||
account_name="_Test Payroll Payable",
|
||||
company="_Test Company",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
account_type="Payable",
|
||||
)
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
setup_salary_structure(employee1, company)
|
||||
|
||||
if (
|
||||
not frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
|
||||
or frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account")
|
||||
!= "_Test Payroll Payable - _TC"
|
||||
):
|
||||
frappe.db.set_value(
|
||||
"Company", "_Test Company", "default_payroll_payable_account", "_Test Payroll Payable - _TC"
|
||||
)
|
||||
currency = frappe.db.get_value("Company", "_Test Company", "default_currency")
|
||||
|
||||
make_salary_structure(
|
||||
"_Test Salary Structure 1",
|
||||
"Monthly",
|
||||
employee1,
|
||||
company="_Test Company",
|
||||
currency=currency,
|
||||
test_tax=False,
|
||||
)
|
||||
ss = make_salary_structure(
|
||||
"_Test Salary Structure 2",
|
||||
"Monthly",
|
||||
employee2,
|
||||
company="_Test Company",
|
||||
currency=currency,
|
||||
currency=company.default_currency,
|
||||
test_tax=False,
|
||||
)
|
||||
|
||||
@ -220,42 +170,38 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
ssa_doc.append(
|
||||
"payroll_cost_centers", {"cost_center": "_Test Cost Center 2 - _TC", "percentage": 40}
|
||||
)
|
||||
|
||||
ssa_doc.save()
|
||||
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
if not frappe.db.get_value(
|
||||
"Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}
|
||||
):
|
||||
pe = make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account="_Test Payroll Payable - _TC",
|
||||
currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
|
||||
department="cc - _TC",
|
||||
company="_Test Company",
|
||||
payment_account="Cash - _TC",
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
|
||||
je_entries = frappe.db.sql(
|
||||
"""
|
||||
select account, cost_center, debit, credit
|
||||
from `tabJournal Entry Account`
|
||||
where parent=%s
|
||||
order by account, cost_center
|
||||
""",
|
||||
je,
|
||||
)
|
||||
expected_je = (
|
||||
("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
|
||||
("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
|
||||
("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
|
||||
)
|
||||
pe = make_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account="_Test Payroll Payable - _TC",
|
||||
currency=frappe.db.get_value("Company", "_Test Company", "default_currency"),
|
||||
department="cc - _TC",
|
||||
company="_Test Company",
|
||||
payment_account="Cash - _TC",
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
je = frappe.db.get_value("Salary Slip", {"payroll_entry": pe.name}, "journal_entry")
|
||||
je_entries = frappe.db.sql(
|
||||
"""
|
||||
select account, cost_center, debit, credit
|
||||
from `tabJournal Entry Account`
|
||||
where parent=%s
|
||||
order by account, cost_center
|
||||
""",
|
||||
je,
|
||||
)
|
||||
expected_je = (
|
||||
("_Test Payroll Payable - _TC", "Main - _TC", 0.0, 155600.0),
|
||||
("Salary - _TC", "_Test Cost Center - _TC", 124800.0, 0.0),
|
||||
("Salary - _TC", "_Test Cost Center 2 - _TC", 31200.0, 0.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center - _TC", 0.0, 320.0),
|
||||
("Salary Deductions - _TC", "_Test Cost Center 2 - _TC", 0.0, 80.0),
|
||||
)
|
||||
|
||||
self.assertEqual(je_entries, expected_je)
|
||||
self.assertEqual(je_entries, expected_je)
|
||||
|
||||
def test_get_end_date(self):
|
||||
self.assertEqual(get_end_date("2017-01-01", "monthly"), {"end_date": "2017-01-31"})
|
||||
@ -268,31 +214,22 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
self.assertEqual(get_end_date("2017-02-15", "daily"), {"end_date": "2017-02-15"})
|
||||
|
||||
def test_loan(self):
|
||||
branch = "Test Employee Branch"
|
||||
applicant = make_employee("test_employee@loan.com", company="_Test Company")
|
||||
company = "_Test Company"
|
||||
holiday_list = make_holiday("test holiday for loan")
|
||||
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
if not company_doc.default_payroll_payable_account:
|
||||
company_doc.default_payroll_payable_account = frappe.db.get_value(
|
||||
"Account", {"company": company, "root_type": "Liability", "account_type": ""}, "name"
|
||||
)
|
||||
company_doc.save()
|
||||
branch = "Test Employee Branch"
|
||||
|
||||
if not frappe.db.exists("Branch", branch):
|
||||
frappe.get_doc({"doctype": "Branch", "branch": branch}).insert()
|
||||
holiday_list = make_holiday("test holiday for loan")
|
||||
|
||||
employee_doc = frappe.get_doc("Employee", applicant)
|
||||
employee_doc.branch = branch
|
||||
employee_doc.holiday_list = holiday_list
|
||||
employee_doc.save()
|
||||
applicant = make_employee(
|
||||
"test_employee@loan.com", company="_Test Company", branch=branch, holiday_list=holiday_list
|
||||
)
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
|
||||
salary_structure = "Test Salary Structure for Loan"
|
||||
make_salary_structure(
|
||||
salary_structure,
|
||||
"Test Salary Structure for Loan",
|
||||
"Monthly",
|
||||
employee=employee_doc.name,
|
||||
employee=applicant,
|
||||
company="_Test Company",
|
||||
currency=company_doc.default_currency,
|
||||
)
|
||||
@ -353,11 +290,110 @@ class TestPayrollEntry(unittest.TestCase):
|
||||
self.assertEqual(row.principal_amount, principal_amount)
|
||||
self.assertEqual(row.total_payment, interest_amount + principal_amount)
|
||||
|
||||
if salary_slip.docstatus == 0:
|
||||
frappe.delete_doc("Salary Slip", name)
|
||||
def test_salary_slip_operation_queueing(self):
|
||||
company = "_Test Company"
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
employee = make_employee("test_employee@payroll.com", company=company)
|
||||
setup_salary_structure(employee, company_doc)
|
||||
|
||||
# enqueue salary slip creation via payroll entry
|
||||
# Payroll Entry status should change to Queued
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = get_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
company=company_doc.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
frappe.flags.enqueue_payroll_entry = True
|
||||
payroll_entry.submit()
|
||||
payroll_entry.reload()
|
||||
|
||||
self.assertEqual(payroll_entry.status, "Queued")
|
||||
frappe.flags.enqueue_payroll_entry = False
|
||||
|
||||
def test_salary_slip_operation_failure(self):
|
||||
company = "_Test Company"
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
employee = make_employee("test_employee@payroll.com", company=company)
|
||||
|
||||
salary_structure = make_salary_structure(
|
||||
"_Test Salary Structure",
|
||||
"Monthly",
|
||||
employee,
|
||||
company=company,
|
||||
currency=company_doc.default_currency,
|
||||
)
|
||||
|
||||
# reset account in component to test submission failure
|
||||
component = frappe.get_doc("Salary Component", salary_structure.earnings[0].salary_component)
|
||||
component.accounts = []
|
||||
component.save()
|
||||
|
||||
# salary slip submission via payroll entry
|
||||
# Payroll Entry status should change to Failed because of the missing account setup
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = get_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
company=company_doc.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
|
||||
# set employee as Inactive to check creation failure
|
||||
frappe.db.set_value("Employee", employee, "status", "Inactive")
|
||||
payroll_entry.submit()
|
||||
payroll_entry.reload()
|
||||
self.assertEqual(payroll_entry.status, "Failed")
|
||||
self.assertIsNotNone(payroll_entry.error_message)
|
||||
|
||||
frappe.db.set_value("Employee", employee, "status", "Active")
|
||||
payroll_entry.submit()
|
||||
payroll_entry.submit_salary_slips()
|
||||
|
||||
payroll_entry.reload()
|
||||
self.assertEqual(payroll_entry.status, "Failed")
|
||||
self.assertIsNotNone(payroll_entry.error_message)
|
||||
|
||||
# set accounts
|
||||
for data in frappe.get_all("Salary Component", pluck="name"):
|
||||
set_salary_component_account(data, company_list=[company])
|
||||
|
||||
# Payroll Entry successful, status should change to Submitted
|
||||
payroll_entry.submit_salary_slips()
|
||||
payroll_entry.reload()
|
||||
|
||||
self.assertEqual(payroll_entry.status, "Submitted")
|
||||
self.assertEqual(payroll_entry.error_message, "")
|
||||
|
||||
def test_payroll_entry_status(self):
|
||||
company = "_Test Company"
|
||||
company_doc = frappe.get_doc("Company", company)
|
||||
employee = make_employee("test_employee@payroll.com", company=company)
|
||||
|
||||
setup_salary_structure(employee, company_doc)
|
||||
|
||||
dates = get_start_end_dates("Monthly", nowdate())
|
||||
payroll_entry = get_payroll_entry(
|
||||
start_date=dates.start_date,
|
||||
end_date=dates.end_date,
|
||||
payable_account=company_doc.default_payroll_payable_account,
|
||||
currency=company_doc.default_currency,
|
||||
company=company_doc.name,
|
||||
cost_center="Main - _TC",
|
||||
)
|
||||
payroll_entry.submit()
|
||||
self.assertEqual(payroll_entry.status, "Submitted")
|
||||
|
||||
payroll_entry.cancel()
|
||||
self.assertEqual(payroll_entry.status, "Cancelled")
|
||||
|
||||
|
||||
def make_payroll_entry(**args):
|
||||
def get_payroll_entry(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
payroll_entry = frappe.new_doc("Payroll Entry")
|
||||
@ -380,8 +416,17 @@ def make_payroll_entry(**args):
|
||||
payroll_entry.payment_account = args.payment_account
|
||||
|
||||
payroll_entry.fill_employee_details()
|
||||
payroll_entry.save()
|
||||
payroll_entry.create_salary_slips()
|
||||
payroll_entry.insert()
|
||||
|
||||
# Commit so that the first salary slip creation failure does not rollback the Payroll Entry insert.
|
||||
frappe.db.commit() # nosemgrep
|
||||
|
||||
return payroll_entry
|
||||
|
||||
|
||||
def make_payroll_entry(**args):
|
||||
payroll_entry = get_payroll_entry(**args)
|
||||
payroll_entry.submit()
|
||||
payroll_entry.submit_salary_slips()
|
||||
if payroll_entry.get_sal_slip_list(ss_status=1):
|
||||
payroll_entry.make_payment_entry()
|
||||
@ -423,10 +468,17 @@ def make_holiday(holiday_list_name):
|
||||
return holiday_list_name
|
||||
|
||||
|
||||
def get_salary_slip(user, period, salary_structure):
|
||||
salary_slip = make_employee_salary_slip(user, period, salary_structure)
|
||||
salary_slip.exchange_rate = 70
|
||||
salary_slip.calculate_net_pay()
|
||||
salary_slip.db_update()
|
||||
def setup_salary_structure(employee, company_doc, currency=None, salary_structure=None):
|
||||
for data in frappe.get_all("Salary Component", pluck="name"):
|
||||
if not frappe.db.get_value(
|
||||
"Salary Component Account", {"parent": data, "company": company_doc.name}, "name"
|
||||
):
|
||||
set_salary_component_account(data)
|
||||
|
||||
return salary_slip
|
||||
make_salary_structure(
|
||||
salary_structure or "_Test Salary Structure",
|
||||
"Monthly",
|
||||
employee,
|
||||
company=company_doc.name,
|
||||
currency=(currency or company_doc.default_currency),
|
||||
)
|
||||
|
@ -29,6 +29,9 @@ from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
|
||||
calculate_amounts,
|
||||
create_repayment_entry,
|
||||
)
|
||||
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
|
||||
process_loan_interest_accrual_for_term_loans,
|
||||
)
|
||||
from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
|
||||
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import (
|
||||
get_benefit_component_amount,
|
||||
@ -116,10 +119,10 @@ class SalarySlip(TransactionBase):
|
||||
self.update_payment_status_for_gratuity()
|
||||
|
||||
def update_payment_status_for_gratuity(self):
|
||||
add_salary = frappe.db.get_all(
|
||||
additional_salary = frappe.db.get_all(
|
||||
"Additional Salary",
|
||||
filters={
|
||||
"payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
|
||||
"payroll_date": ("between", [self.start_date, self.end_date]),
|
||||
"employee": self.employee,
|
||||
"ref_doctype": "Gratuity",
|
||||
"docstatus": 1,
|
||||
@ -128,10 +131,10 @@ class SalarySlip(TransactionBase):
|
||||
limit=1,
|
||||
)
|
||||
|
||||
if len(add_salary):
|
||||
if additional_salary:
|
||||
status = "Paid" if self.docstatus == 1 else "Unpaid"
|
||||
if add_salary[0].name in [data.additional_salary for data in self.earnings]:
|
||||
frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
|
||||
if additional_salary[0].name in [entry.additional_salary for entry in self.earnings]:
|
||||
frappe.db.set_value("Gratuity", additional_salary[0].ref_docname, "status", status)
|
||||
|
||||
def on_cancel(self):
|
||||
self.set_status()
|
||||
@ -1364,9 +1367,9 @@ class SalarySlip(TransactionBase):
|
||||
self.total_loan_repayment += payment.total_payment
|
||||
|
||||
def get_loan_details(self):
|
||||
return frappe.get_all(
|
||||
loan_details = frappe.get_all(
|
||||
"Loan",
|
||||
fields=["name", "interest_income_account", "loan_account", "loan_type"],
|
||||
fields=["name", "interest_income_account", "loan_account", "loan_type", "is_term_loan"],
|
||||
filters={
|
||||
"applicant": self.employee,
|
||||
"docstatus": 1,
|
||||
@ -1375,6 +1378,15 @@ class SalarySlip(TransactionBase):
|
||||
},
|
||||
)
|
||||
|
||||
if loan_details:
|
||||
for loan in loan_details:
|
||||
if loan.is_term_loan:
|
||||
process_loan_interest_accrual_for_term_loans(
|
||||
posting_date=self.posting_date, loan_type=loan.loan_type, loan=loan.name
|
||||
)
|
||||
|
||||
return loan_details
|
||||
|
||||
def make_loan_repayment_entry(self):
|
||||
payroll_payable_account = get_payroll_payable_account(self.company, self.payroll_entry)
|
||||
for loan in self.loans:
|
||||
|
@ -997,7 +997,7 @@ class TestSalarySlip(unittest.TestCase):
|
||||
return [no_of_days_in_month[1], no_of_holidays_in_month]
|
||||
|
||||
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
def make_employee_salary_slip(user, payroll_frequency, salary_structure=None, posting_date=None):
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
if not salary_structure:
|
||||
@ -1008,7 +1008,11 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
)
|
||||
|
||||
salary_structure_doc = make_salary_structure(
|
||||
salary_structure, payroll_frequency, employee=employee.name, company=employee.company
|
||||
salary_structure,
|
||||
payroll_frequency,
|
||||
employee=employee.name,
|
||||
company=employee.company,
|
||||
from_date=posting_date,
|
||||
)
|
||||
salary_slip_name = frappe.db.get_value(
|
||||
"Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})}
|
||||
@ -1018,7 +1022,7 @@ def make_employee_salary_slip(user, payroll_frequency, salary_structure=None):
|
||||
salary_slip = make_salary_slip(salary_structure_doc.name, employee=employee.name)
|
||||
salary_slip.employee_name = employee.employee_name
|
||||
salary_slip.payroll_frequency = payroll_frequency
|
||||
salary_slip.posting_date = nowdate()
|
||||
salary_slip.posting_date = posting_date or nowdate()
|
||||
salary_slip.insert()
|
||||
else:
|
||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
|
||||
@ -1046,10 +1050,10 @@ def make_salary_component(salary_components, test_tax, company_list=None):
|
||||
doc.update(salary_component)
|
||||
doc.insert()
|
||||
|
||||
get_salary_component_account(doc, company_list)
|
||||
set_salary_component_account(doc, company_list)
|
||||
|
||||
|
||||
def get_salary_component_account(sal_comp, company_list=None):
|
||||
def set_salary_component_account(sal_comp, company_list=None):
|
||||
company = erpnext.get_default_company()
|
||||
|
||||
if company_list and company not in company_list:
|
||||
|
@ -253,6 +253,7 @@ def make_salary_slip(
|
||||
source_name,
|
||||
target_doc=None,
|
||||
employee=None,
|
||||
posting_date=None,
|
||||
as_print=False,
|
||||
print_format=None,
|
||||
for_preview=0,
|
||||
@ -269,6 +270,9 @@ def make_salary_slip(
|
||||
target.designation = employee_details.designation
|
||||
target.department = employee_details.department
|
||||
|
||||
if posting_date:
|
||||
target.posting_date = posting_date
|
||||
|
||||
target.run_method("process_salary_structure", for_preview=for_preview)
|
||||
|
||||
doc = get_mapped_doc(
|
||||
|
@ -169,9 +169,6 @@ def make_salary_structure(
|
||||
payroll_period=None,
|
||||
include_flexi_benefits=False,
|
||||
):
|
||||
if test_tax:
|
||||
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""", (salary_structure))
|
||||
|
||||
if frappe.db.exists("Salary Structure", salary_structure):
|
||||
frappe.db.delete("Salary Structure", salary_structure)
|
||||
|
||||
@ -230,9 +227,12 @@ def create_salary_structure_assignment(
|
||||
company=None,
|
||||
currency=erpnext.get_default_currency(),
|
||||
payroll_period=None,
|
||||
base=None,
|
||||
allow_duplicate=False,
|
||||
):
|
||||
|
||||
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
|
||||
if not allow_duplicate and frappe.db.exists(
|
||||
"Salary Structure Assignment", {"employee": employee}
|
||||
):
|
||||
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""", (employee))
|
||||
|
||||
if not payroll_period:
|
||||
@ -245,7 +245,7 @@ def create_salary_structure_assignment(
|
||||
|
||||
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
|
||||
salary_structure_assignment.employee = employee
|
||||
salary_structure_assignment.base = 50000
|
||||
salary_structure_assignment.base = base or 50000
|
||||
salary_structure_assignment.variable = 5000
|
||||
|
||||
if not from_date:
|
||||
|
@ -423,7 +423,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
item.barcode = null;
|
||||
|
||||
|
||||
if(item.item_code || item.barcode || item.serial_no) {
|
||||
if(item.item_code || item.serial_no) {
|
||||
if(!this.validate_company_and_party()) {
|
||||
this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove();
|
||||
} else {
|
||||
@ -463,6 +463,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
stock_qty: item.stock_qty,
|
||||
conversion_factor: item.conversion_factor,
|
||||
weight_per_unit: item.weight_per_unit,
|
||||
uom: item.uom,
|
||||
weight_uom: item.weight_uom,
|
||||
manufacturer: item.manufacturer,
|
||||
stock_uom: item.stock_uom,
|
||||
@ -526,12 +527,6 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
if(!d[k]) d[k] = v;
|
||||
});
|
||||
|
||||
if (d.__disable_batch_serial_selector) {
|
||||
// reset for future use.
|
||||
d.__disable_batch_serial_selector = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (d.has_batch_no && d.has_serial_no) {
|
||||
d.batch_no = undefined;
|
||||
}
|
||||
@ -944,7 +939,11 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
} else {
|
||||
// company currency and doc currency is same
|
||||
// this will prevent unnecessary conversion rate triggers
|
||||
this.frm.set_value("conversion_rate", 1.0);
|
||||
if(this.frm.doc.currency === this.get_company_currency()) {
|
||||
this.frm.set_value("conversion_rate", 1.0);
|
||||
} else {
|
||||
this.conversion_rate();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,6 +9,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
this.barcode_field = opts.barcode_field || "barcode";
|
||||
this.serial_no_field = opts.serial_no_field || "serial_no";
|
||||
this.batch_no_field = opts.batch_no_field || "batch_no";
|
||||
this.uom_field = opts.uom_field || "uom";
|
||||
this.qty_field = opts.qty_field || "qty";
|
||||
// field name on row which defines max quantity to be scanned e.g. picklist
|
||||
this.max_qty_field = opts.max_qty_field;
|
||||
@ -26,6 +27,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
// bar_code: "123456", // present if barcode was scanned
|
||||
// batch_no: "LOT12", // present if batch was scanned
|
||||
// serial_no: "987XYZ", // present if serial no was scanned
|
||||
// uom: "Kg", // present if barcode UOM is different from default
|
||||
// }
|
||||
this.scan_api = opts.scan_api || "erpnext.stock.utils.scan_barcode";
|
||||
}
|
||||
@ -35,6 +37,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
let me = this;
|
||||
|
||||
const input = this.scan_barcode_field.value;
|
||||
this.scan_barcode_field.set_value("");
|
||||
if (!input) {
|
||||
return;
|
||||
}
|
||||
@ -55,87 +58,92 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
return;
|
||||
}
|
||||
|
||||
const row = me.update_table(data);
|
||||
if (row) {
|
||||
resolve(row);
|
||||
}
|
||||
else {
|
||||
reject();
|
||||
}
|
||||
me.update_table(data).then(row => {
|
||||
row ? resolve(row) : reject();
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
update_table(data) {
|
||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||
return new Promise(resolve => {
|
||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||
|
||||
const {item_code, barcode, batch_no, serial_no} = data;
|
||||
const {item_code, barcode, batch_no, serial_no, uom} = data;
|
||||
|
||||
let row = this.get_row_to_modify_on_scan(item_code, batch_no);
|
||||
let row = this.get_row_to_modify_on_scan(item_code, batch_no, uom);
|
||||
|
||||
if (!row) {
|
||||
if (this.dont_allow_new_row) {
|
||||
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
|
||||
if (!row) {
|
||||
if (this.dont_allow_new_row) {
|
||||
this.show_alert(__("Maximum quantity scanned for item {0}.", [item_code]), "red");
|
||||
this.clean_up();
|
||||
return;
|
||||
}
|
||||
|
||||
// add new row if new item/batch is scanned
|
||||
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
||||
// trigger any row add triggers defined on child table.
|
||||
this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name);
|
||||
}
|
||||
|
||||
if (this.is_duplicate_serial_no(row, serial_no)) {
|
||||
this.clean_up();
|
||||
return;
|
||||
}
|
||||
|
||||
// add new row if new item/batch is scanned
|
||||
row = frappe.model.add_child(this.frm.doc, cur_grid.doctype, this.items_table_name);
|
||||
// trigger any row add triggers defined on child table.
|
||||
this.frm.script_manager.trigger(`${this.items_table_name}_add`, row.doctype, row.name);
|
||||
}
|
||||
|
||||
if (this.is_duplicate_serial_no(row, serial_no)) {
|
||||
this.clean_up();
|
||||
return;
|
||||
}
|
||||
|
||||
this.set_selector_trigger_flag(row, data);
|
||||
this.set_item(row, item_code).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
frappe.run_serially([
|
||||
() => this.set_selector_trigger_flag(data),
|
||||
() => this.set_item(row, item_code).then(qty => {
|
||||
this.show_scan_message(row.idx, row.item_code, qty);
|
||||
}),
|
||||
() => this.set_barcode_uom(row, uom),
|
||||
() => this.set_serial_no(row, serial_no),
|
||||
() => this.set_batch_no(row, batch_no),
|
||||
() => this.set_barcode(row, barcode),
|
||||
() => this.clean_up(),
|
||||
() => this.revert_selector_flag(),
|
||||
() => resolve(row)
|
||||
]);
|
||||
});
|
||||
this.set_serial_no(row, serial_no);
|
||||
this.set_batch_no(row, batch_no);
|
||||
this.set_barcode(row, barcode);
|
||||
this.clean_up();
|
||||
return row;
|
||||
}
|
||||
|
||||
// batch and serial selector is reduandant when all info can be added by scan
|
||||
// this flag on item row is used by transaction.js to avoid triggering selector
|
||||
set_selector_trigger_flag(row, data) {
|
||||
set_selector_trigger_flag(data) {
|
||||
const {batch_no, serial_no, has_batch_no, has_serial_no} = data;
|
||||
|
||||
const require_selecting_batch = has_batch_no && !batch_no;
|
||||
const require_selecting_serial = has_serial_no && !serial_no;
|
||||
|
||||
if (!(require_selecting_batch || require_selecting_serial)) {
|
||||
row.__disable_batch_serial_selector = true;
|
||||
frappe.flags.hide_serial_batch_dialog = true;
|
||||
}
|
||||
}
|
||||
|
||||
revert_selector_flag() {
|
||||
frappe.flags.hide_serial_batch_dialog = false;
|
||||
}
|
||||
|
||||
set_item(row, item_code) {
|
||||
return new Promise(resolve => {
|
||||
const increment = (value = 1) => {
|
||||
const increment = async (value = 1) => {
|
||||
const item_data = {item_code: item_code};
|
||||
item_data[this.qty_field] = Number((row[this.qty_field] || 0)) + Number(value);
|
||||
frappe.model.set_value(row.doctype, row.name, item_data);
|
||||
await frappe.model.set_value(row.doctype, row.name, item_data);
|
||||
return value;
|
||||
};
|
||||
|
||||
if (this.prompt_qty) {
|
||||
frappe.prompt(__("Please enter quantity for item {0}", [item_code]), ({value}) => {
|
||||
increment(value);
|
||||
resolve(value);
|
||||
increment(value).then((value) => resolve(value));
|
||||
});
|
||||
} else {
|
||||
increment();
|
||||
resolve();
|
||||
increment().then((value) => resolve(value));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
set_serial_no(row, serial_no) {
|
||||
async set_serial_no(row, serial_no) {
|
||||
if (serial_no && frappe.meta.has_field(row.doctype, this.serial_no_field)) {
|
||||
const existing_serial_nos = row[this.serial_no_field];
|
||||
let new_serial_nos = "";
|
||||
@ -145,19 +153,25 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
} else {
|
||||
new_serial_nos = serial_no;
|
||||
}
|
||||
frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
|
||||
await frappe.model.set_value(row.doctype, row.name, this.serial_no_field, new_serial_nos);
|
||||
}
|
||||
}
|
||||
|
||||
set_batch_no(row, batch_no) {
|
||||
async set_barcode_uom(row, uom) {
|
||||
if (uom && frappe.meta.has_field(row.doctype, this.uom_field)) {
|
||||
await frappe.model.set_value(row.doctype, row.name, this.uom_field, uom);
|
||||
}
|
||||
}
|
||||
|
||||
async set_batch_no(row, batch_no) {
|
||||
if (batch_no && frappe.meta.has_field(row.doctype, this.batch_no_field)) {
|
||||
frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
|
||||
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
|
||||
}
|
||||
}
|
||||
|
||||
set_barcode(row, barcode) {
|
||||
async set_barcode(row, barcode) {
|
||||
if (barcode && frappe.meta.has_field(row.doctype, this.barcode_field)) {
|
||||
frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
||||
await frappe.model.set_value(row.doctype, row.name, this.barcode_field, barcode);
|
||||
}
|
||||
}
|
||||
|
||||
@ -179,7 +193,7 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
return is_duplicate;
|
||||
}
|
||||
|
||||
get_row_to_modify_on_scan(item_code, batch_no) {
|
||||
get_row_to_modify_on_scan(item_code, batch_no, uom) {
|
||||
let cur_grid = this.frm.fields_dict[this.items_table_name].grid;
|
||||
|
||||
// Check if batch is scanned and table has batch no field
|
||||
@ -188,10 +202,12 @@ erpnext.utils.BarcodeScanner = class BarcodeScanner {
|
||||
|
||||
const matching_row = (row) => {
|
||||
const item_match = row.item_code == item_code;
|
||||
const batch_match = row.batch_no == batch_no;
|
||||
const batch_match = row[this.batch_no_field] == batch_no;
|
||||
const uom_match = !uom || row[this.uom_field] == uom;
|
||||
const qty_in_limit = flt(row[this.qty_field]) < flt(row[this.max_qty_field]);
|
||||
|
||||
return item_match
|
||||
&& uom_match
|
||||
&& (!is_batch_no_scan || batch_match)
|
||||
&& (!check_max_qty || qty_in_limit)
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
|
||||
if (!invoice_eligible) return;
|
||||
|
||||
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
|
||||
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, qrcode_image, __unsaved } = frm.doc;
|
||||
|
||||
const add_custom_button = (label, action) => {
|
||||
if (!frm.custom_buttons[label]) {
|
||||
@ -150,52 +150,72 @@ erpnext.setup_einvoice_actions = (doctype) => {
|
||||
|
||||
if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
|
||||
const action = () => {
|
||||
let message = __('Cancellation of e-way bill is currently not supported.') + ' ';
|
||||
message += '<br><br>';
|
||||
message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.');
|
||||
|
||||
const dialog = frappe.msgprint({
|
||||
title: __('Update E-Way Bill Cancelled Status?'),
|
||||
message: message,
|
||||
indicator: 'orange',
|
||||
primary_action: {
|
||||
action: function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() && dialog.hide()
|
||||
});
|
||||
}
|
||||
// This confirm is added to just reduce unnecesory API calls. All required logic is implemented on server side.
|
||||
frappe.confirm(
|
||||
__("Have you cancelled e-way bill on the portal?"),
|
||||
() => {
|
||||
frappe.call({
|
||||
method: "erpnext.regional.india.e_invoice.utils.cancel_eway_bill",
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc(),
|
||||
});
|
||||
},
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
() => {
|
||||
frappe.show_alert(
|
||||
{
|
||||
message: __(
|
||||
"Please cancel e-way bill on the portal first."
|
||||
),
|
||||
indicator: "orange",
|
||||
},
|
||||
5
|
||||
);
|
||||
}
|
||||
);
|
||||
};
|
||||
add_custom_button(__("Cancel E-Way Bill"), action);
|
||||
}
|
||||
|
||||
if (irn && !irn_cancelled) {
|
||||
const action = () => {
|
||||
const dialog = frappe.msgprint({
|
||||
title: __("Generate QRCode"),
|
||||
message: __("Generate and attach QR Code using IRN?"),
|
||||
primary_action: {
|
||||
action: function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() || dialog.hide(),
|
||||
error: () => dialog.hide()
|
||||
});
|
||||
let is_qrcode_attached = false;
|
||||
if (qrcode_image && frm.attachments) {
|
||||
let attachments = frm.attachments.get_attachments();
|
||||
if (attachments.length != 0) {
|
||||
for (let i = 0; i < attachments.length; i++) {
|
||||
if (attachments[i].file_url == qrcode_image) {
|
||||
is_qrcode_attached = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!is_qrcode_attached) {
|
||||
const action = () => {
|
||||
if (frm.doc.__unsaved) {
|
||||
frappe.throw(__('Please save the document to generate QRCode.'));
|
||||
}
|
||||
const dialog = frappe.msgprint({
|
||||
title: __("Generate QRCode"),
|
||||
message: __("Generate and attach QR Code using IRN?"),
|
||||
primary_action: {
|
||||
action: function() {
|
||||
frappe.call({
|
||||
method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode',
|
||||
args: { doctype, docname: name },
|
||||
freeze: true,
|
||||
callback: () => frm.reload_doc() || dialog.hide(),
|
||||
error: () => dialog.hide()
|
||||
});
|
||||
}
|
||||
},
|
||||
primary_action_label: __('Yes')
|
||||
});
|
||||
dialog.show();
|
||||
};
|
||||
add_custom_button(__("Generate QRCode"), action);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
@ -803,6 +803,8 @@ class GSPConnector:
|
||||
self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin"
|
||||
# cancel_ewaybill_url will only work if user have bought ewb api from adaequare.
|
||||
self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB"
|
||||
# ewaybill_details_url + ?irn={irn_number} will provide eway bill number and details.
|
||||
self.ewaybill_details_url = self.base_url + "/enriched/ei/api/ewaybill/irn"
|
||||
self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill"
|
||||
self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image"
|
||||
|
||||
@ -1010,13 +1012,32 @@ class GSPConnector:
|
||||
return failed
|
||||
|
||||
def fetch_and_attach_qrcode_from_irn(self):
|
||||
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
|
||||
if qrcode:
|
||||
qrcode_file = self.create_qr_code_file(qrcode)
|
||||
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
|
||||
frappe.msgprint(_("QR Code attached to the invoice"), alert=True)
|
||||
is_qrcode_file_attached = self.invoice.qrcode_image and frappe.db.exists(
|
||||
"File",
|
||||
{
|
||||
"attached_to_doctype": "Sales Invoice",
|
||||
"attached_to_name": self.invoice.name,
|
||||
"file_url": self.invoice.qrcode_image,
|
||||
"attached_to_field": "qrcode_image",
|
||||
},
|
||||
)
|
||||
if not is_qrcode_file_attached:
|
||||
if self.invoice.signed_qr_code:
|
||||
self.attach_qrcode_image()
|
||||
frappe.db.set_value(
|
||||
"Sales Invoice", self.invoice.name, "qrcode_image", self.invoice.qrcode_image
|
||||
)
|
||||
frappe.msgprint(_("QR Code attached to the invoice."), alert=True)
|
||||
else:
|
||||
qrcode = self.get_qrcode_from_irn(self.invoice.irn)
|
||||
if qrcode:
|
||||
qrcode_file = self.create_qr_code_file(qrcode)
|
||||
frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url)
|
||||
frappe.msgprint(_("QR Code attached to the invoice."), alert=True)
|
||||
else:
|
||||
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
|
||||
else:
|
||||
frappe.msgprint(_("QR Code not found for the IRN"), alert=True)
|
||||
frappe.msgprint(_("QR Code is already Attached"), indicator="green", alert=True)
|
||||
|
||||
def get_qrcode_from_irn(self, irn):
|
||||
import requests
|
||||
@ -1186,23 +1207,22 @@ class GSPConnector:
|
||||
log_error(data)
|
||||
self.raise_error(True)
|
||||
|
||||
def cancel_eway_bill(self, eway_bill, reason, remark):
|
||||
def get_ewb_details(self):
|
||||
"""
|
||||
Get e-Waybill Details by IRN API documentaion for validation is not added yet.
|
||||
https://einv-apisandbox.nic.in/version1.03/get-ewaybill-details-by-irn.html#validations
|
||||
NOTE: if ewaybill Validity period lapsed or scanned by officer enroute (not tested yet) it will still return status as "ACT".
|
||||
"""
|
||||
headers = self.get_headers()
|
||||
data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4)
|
||||
headers["username"] = headers["user_name"]
|
||||
del headers["user_name"]
|
||||
try:
|
||||
res = self.make_request("post", self.cancel_ewaybill_url, headers, data)
|
||||
if res.get("success"):
|
||||
self.invoice.ewaybill = ""
|
||||
self.invoice.eway_bill_cancelled = 1
|
||||
self.invoice.flags.updater_reference = {
|
||||
"doctype": self.invoice.doctype,
|
||||
"docname": self.invoice.name,
|
||||
"label": _("E-Way Bill Cancelled - {}").format(remark),
|
||||
}
|
||||
self.update_invoice()
|
||||
irn = self.invoice.irn
|
||||
if not irn:
|
||||
frappe.throw(_("IRN is mandatory to get E-Waybill Details. Please generate IRN first."))
|
||||
|
||||
try:
|
||||
params = "?irn={irn}".format(irn=irn)
|
||||
res = self.make_request("get", self.ewaybill_details_url + params, headers)
|
||||
if res.get("success"):
|
||||
return res.get("result")
|
||||
else:
|
||||
raise RequestFailed
|
||||
|
||||
@ -1211,9 +1231,65 @@ class GSPConnector:
|
||||
self.raise_error(errors=errors)
|
||||
|
||||
except Exception:
|
||||
log_error(data)
|
||||
log_error()
|
||||
self.raise_error(True)
|
||||
|
||||
def update_ewb_details(self, ewb_details=None):
|
||||
# for any reason user chooses to generate eway bill using portal this will allow to update ewaybill details in the invoice.
|
||||
if not self.invoice.irn:
|
||||
frappe.throw(_("IRN is mandatory to update E-Waybill Details. Please generate IRN first."))
|
||||
if not ewb_details:
|
||||
ewb_details = self.get_ewb_details()
|
||||
if ewb_details:
|
||||
self.invoice.ewaybill = ewb_details.get("EwbNo")
|
||||
self.invoice.eway_bill_validity = ewb_details.get("EwbValidTill")
|
||||
self.invoice.eway_bill_cancelled = 0 if ewb_details.get("Status") == "ACT" else 1
|
||||
self.update_invoice()
|
||||
|
||||
def cancel_eway_bill(self):
|
||||
ewb_details = self.get_ewb_details()
|
||||
if ewb_details:
|
||||
ewb_no = str(ewb_details.get("EwbNo"))
|
||||
ewb_status = ewb_details.get("Status")
|
||||
if ewb_status == "CNL":
|
||||
self.invoice.ewaybill = ""
|
||||
self.invoice.eway_bill_cancelled = 1
|
||||
self.invoice.flags.updater_reference = {
|
||||
"doctype": self.invoice.doctype,
|
||||
"docname": self.invoice.name,
|
||||
"label": _("E-Way Bill Cancelled"),
|
||||
}
|
||||
self.update_invoice()
|
||||
frappe.msgprint(
|
||||
_("E-Way Bill Cancelled successfully"),
|
||||
indicator="green",
|
||||
alert=True,
|
||||
)
|
||||
elif ewb_status == "ACT" and self.invoice.ewaybill == ewb_no:
|
||||
msg = _("E-Way Bill {} is still active.").format(bold(ewb_no))
|
||||
msg += "<br><br>"
|
||||
msg += _(
|
||||
"You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system."
|
||||
)
|
||||
frappe.msgprint(msg)
|
||||
elif ewb_status == "ACT" and self.invoice.ewaybill != ewb_no:
|
||||
# if user cancelled the current eway bill and generated new eway bill using portal, then this will update new ewb number in sales invoice.
|
||||
msg = _("E-Way Bill No. {0} doesn't match {1} saved in the invoice.").format(
|
||||
bold(ewb_no), bold(self.invoice.ewaybill)
|
||||
)
|
||||
msg += "<hr/>"
|
||||
msg += _("E-Way Bill No. {} is updated in the invoice.").format(bold(ewb_no))
|
||||
frappe.msgprint(msg)
|
||||
self.update_ewb_details(ewb_details=ewb_details)
|
||||
else:
|
||||
# this block should not be ever called but added incase there is any change in API.
|
||||
msg = _("Unknown E-Way Status Code {}.").format(ewb_status)
|
||||
msg += "<br><br>"
|
||||
msg += _("Please contact your system administrator.")
|
||||
frappe.throw(msg)
|
||||
else:
|
||||
frappe.msgprint(_("E-Way Bill Details not found for this IRN."))
|
||||
|
||||
def sanitize_error_message(self, message):
|
||||
"""
|
||||
On validation errors, response message looks something like this:
|
||||
@ -1281,7 +1357,6 @@ class GSPConnector:
|
||||
|
||||
def attach_qrcode_image(self):
|
||||
qrcode = self.invoice.signed_qr_code
|
||||
|
||||
qr_image = io.BytesIO()
|
||||
url = qrcreate(qrcode, error="L")
|
||||
url.png(qr_image, scale=2, quiet_zone=1)
|
||||
@ -1365,12 +1440,22 @@ def generate_eway_bill(doctype, docname, **kwargs):
|
||||
|
||||
@frappe.whitelist()
|
||||
def cancel_eway_bill(doctype, docname):
|
||||
# NOTE: cancel_eway_bill api is disabled by Adequare.
|
||||
# gsp_connector = GSPConnector(doctype, docname)
|
||||
# gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
|
||||
# NOTE: cancel_eway_bill api is disabled by NIC for E-invoice so this will only check if eway bill is canceled or not and update accordingly.
|
||||
# https://einv-apisandbox.nic.in/version1.03/cancel-eway-bill.html#
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.cancel_eway_bill()
|
||||
|
||||
frappe.db.set_value(doctype, docname, "ewaybill", "")
|
||||
frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_ewb_details(doctype, docname):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.get_ewb_details()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_ewb_details(doctype, docname):
|
||||
gsp_connector = GSPConnector(doctype, docname)
|
||||
gsp_connector.update_ewb_details()
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -1,14 +1,24 @@
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.utils import get_fetch_values
|
||||
from frappe.utils import cint, cstr, date_diff, flt, getdate, nowdate
|
||||
from frappe.utils import (
|
||||
add_days,
|
||||
cint,
|
||||
cstr,
|
||||
date_diff,
|
||||
flt,
|
||||
get_link_to_form,
|
||||
getdate,
|
||||
month_diff,
|
||||
)
|
||||
|
||||
from erpnext.controllers.accounts_controller import get_taxes_and_charges
|
||||
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
|
||||
from erpnext.hr.utils import get_salary_assignment
|
||||
from erpnext.hr.utils import get_salary_assignments
|
||||
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
|
||||
from erpnext.regional.india import number_state_mapping, state_numbers, states
|
||||
|
||||
@ -359,45 +369,57 @@ def calculate_annual_eligible_hra_exemption(doc):
|
||||
basic_component, hra_component = frappe.db.get_value(
|
||||
"Company", doc.company, ["basic_component", "hra_component"]
|
||||
)
|
||||
|
||||
if not (basic_component and hra_component):
|
||||
frappe.throw(_("Please mention Basic and HRA component in Company"))
|
||||
annual_exemption, monthly_exemption, hra_amount = 0, 0, 0
|
||||
frappe.throw(
|
||||
_("Please set Basic and HRA component in Company {0}").format(
|
||||
get_link_to_form("Company", doc.company)
|
||||
)
|
||||
)
|
||||
|
||||
annual_exemption = monthly_exemption = hra_amount = basic_amount = 0
|
||||
|
||||
if hra_component and basic_component:
|
||||
assignment = get_salary_assignment(doc.employee, nowdate())
|
||||
if assignment:
|
||||
hra_component_exists = frappe.db.exists(
|
||||
"Salary Detail",
|
||||
{
|
||||
"parent": assignment.salary_structure,
|
||||
"salary_component": hra_component,
|
||||
"parentfield": "earnings",
|
||||
"parenttype": "Salary Structure",
|
||||
},
|
||||
)
|
||||
assignments = get_salary_assignments(doc.employee, doc.payroll_period)
|
||||
|
||||
if hra_component_exists:
|
||||
basic_amount, hra_amount = get_component_amt_from_salary_slip(
|
||||
doc.employee, assignment.salary_structure, basic_component, hra_component
|
||||
)
|
||||
if hra_amount:
|
||||
if doc.monthly_house_rent:
|
||||
annual_exemption = calculate_hra_exemption(
|
||||
assignment.salary_structure,
|
||||
basic_amount,
|
||||
hra_amount,
|
||||
doc.monthly_house_rent,
|
||||
doc.rented_in_metro_city,
|
||||
)
|
||||
if annual_exemption > 0:
|
||||
monthly_exemption = annual_exemption / 12
|
||||
else:
|
||||
annual_exemption = 0
|
||||
|
||||
elif doc.docstatus == 1:
|
||||
if not assignments and doc.docstatus == 1:
|
||||
frappe.throw(
|
||||
_("Salary Structure must be submitted before submission of Tax Ememption Declaration")
|
||||
_("Salary Structure must be submitted before submission of {0}").format(doc.doctype)
|
||||
)
|
||||
|
||||
assignment_dates = [assignment.from_date for assignment in assignments]
|
||||
|
||||
for idx, assignment in enumerate(assignments):
|
||||
if has_hra_component(assignment.salary_structure, hra_component):
|
||||
basic_salary_amt, hra_salary_amt = get_component_amt_from_salary_slip(
|
||||
doc.employee,
|
||||
assignment.salary_structure,
|
||||
basic_component,
|
||||
hra_component,
|
||||
assignment.from_date,
|
||||
)
|
||||
to_date = get_end_date_for_assignment(assignment_dates, idx, doc.payroll_period)
|
||||
|
||||
frequency = frappe.get_value(
|
||||
"Salary Structure", assignment.salary_structure, "payroll_frequency"
|
||||
)
|
||||
basic_amount += get_component_pay(frequency, basic_salary_amt, assignment.from_date, to_date)
|
||||
hra_amount += get_component_pay(frequency, hra_salary_amt, assignment.from_date, to_date)
|
||||
|
||||
if hra_amount:
|
||||
if doc.monthly_house_rent:
|
||||
annual_exemption = calculate_hra_exemption(
|
||||
assignment.salary_structure,
|
||||
basic_amount,
|
||||
hra_amount,
|
||||
doc.monthly_house_rent,
|
||||
doc.rented_in_metro_city,
|
||||
)
|
||||
if annual_exemption > 0:
|
||||
monthly_exemption = annual_exemption / 12
|
||||
else:
|
||||
annual_exemption = 0
|
||||
|
||||
return frappe._dict(
|
||||
{
|
||||
"hra_amount": hra_amount,
|
||||
@ -407,10 +429,44 @@ def calculate_annual_eligible_hra_exemption(doc):
|
||||
)
|
||||
|
||||
|
||||
def get_component_amt_from_salary_slip(employee, salary_structure, basic_component, hra_component):
|
||||
salary_slip = make_salary_slip(
|
||||
salary_structure, employee=employee, for_preview=1, ignore_permissions=True
|
||||
def has_hra_component(salary_structure, hra_component):
|
||||
return frappe.db.exists(
|
||||
"Salary Detail",
|
||||
{
|
||||
"parent": salary_structure,
|
||||
"salary_component": hra_component,
|
||||
"parentfield": "earnings",
|
||||
"parenttype": "Salary Structure",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def get_end_date_for_assignment(assignment_dates, idx, payroll_period):
|
||||
end_date = None
|
||||
|
||||
try:
|
||||
end_date = assignment_dates[idx + 1]
|
||||
end_date = add_days(end_date, -1)
|
||||
except IndexError:
|
||||
pass
|
||||
|
||||
if not end_date:
|
||||
end_date = frappe.db.get_value("Payroll Period", payroll_period, "end_date")
|
||||
|
||||
return end_date
|
||||
|
||||
|
||||
def get_component_amt_from_salary_slip(
|
||||
employee, salary_structure, basic_component, hra_component, from_date
|
||||
):
|
||||
salary_slip = make_salary_slip(
|
||||
salary_structure,
|
||||
employee=employee,
|
||||
for_preview=1,
|
||||
ignore_permissions=True,
|
||||
posting_date=from_date,
|
||||
)
|
||||
|
||||
basic_amt, hra_amt = 0, 0
|
||||
for earning in salary_slip.earnings:
|
||||
if earning.salary_component == basic_component:
|
||||
@ -423,36 +479,37 @@ def get_component_amt_from_salary_slip(employee, salary_structure, basic_compone
|
||||
|
||||
|
||||
def calculate_hra_exemption(
|
||||
salary_structure, basic, monthly_hra, monthly_house_rent, rented_in_metro_city
|
||||
salary_structure, annual_basic, annual_hra, monthly_house_rent, rented_in_metro_city
|
||||
):
|
||||
# TODO make this configurable
|
||||
exemptions = []
|
||||
frequency = frappe.get_value("Salary Structure", salary_structure, "payroll_frequency")
|
||||
# case 1: The actual amount allotted by the employer as the HRA.
|
||||
exemptions.append(get_annual_component_pay(frequency, monthly_hra))
|
||||
|
||||
actual_annual_rent = monthly_house_rent * 12
|
||||
annual_basic = get_annual_component_pay(frequency, basic)
|
||||
exemptions.append(annual_hra)
|
||||
|
||||
# case 2: Actual rent paid less 10% of the basic salary.
|
||||
actual_annual_rent = monthly_house_rent * 12
|
||||
exemptions.append(flt(actual_annual_rent) - flt(annual_basic * 0.1))
|
||||
|
||||
# case 3: 50% of the basic salary, if the employee is staying in a metro city (40% for a non-metro city).
|
||||
exemptions.append(annual_basic * 0.5 if rented_in_metro_city else annual_basic * 0.4)
|
||||
|
||||
# return minimum of 3 cases
|
||||
return min(exemptions)
|
||||
|
||||
|
||||
def get_annual_component_pay(frequency, amount):
|
||||
def get_component_pay(frequency, amount, from_date, to_date):
|
||||
days = date_diff(to_date, from_date) + 1
|
||||
|
||||
if frequency == "Daily":
|
||||
return amount * 365
|
||||
return amount * days
|
||||
elif frequency == "Weekly":
|
||||
return amount * 52
|
||||
return amount * math.floor(days / 7)
|
||||
elif frequency == "Fortnightly":
|
||||
return amount * 26
|
||||
return amount * math.floor(days / 14)
|
||||
elif frequency == "Monthly":
|
||||
return amount * 12
|
||||
return amount * month_diff(to_date, from_date)
|
||||
elif frequency == "Bimonthly":
|
||||
return amount * 6
|
||||
return amount * (month_diff(to_date, from_date) / 2)
|
||||
|
||||
|
||||
def validate_house_rent_dates(doc):
|
||||
|
@ -783,6 +783,7 @@ class TestSalesOrder(FrappeTestCase):
|
||||
|
||||
def test_auto_insert_price(self):
|
||||
make_item("_Test Item for Auto Price List", {"is_stock_item": 0})
|
||||
make_item("_Test Item for Auto Price List with Discount Percentage", {"is_stock_item": 0})
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 1)
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
@ -804,6 +805,25 @@ class TestSalesOrder(FrappeTestCase):
|
||||
100,
|
||||
)
|
||||
|
||||
make_sales_order(
|
||||
item_code="_Test Item for Auto Price List with Discount Percentage",
|
||||
selling_price_list="_Test Price List",
|
||||
price_list_rate=200,
|
||||
discount_percentage=20,
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
frappe.db.get_value(
|
||||
"Item Price",
|
||||
{
|
||||
"price_list": "_Test Price List",
|
||||
"item_code": "_Test Item for Auto Price List with Discount Percentage",
|
||||
},
|
||||
"price_list_rate",
|
||||
),
|
||||
200,
|
||||
)
|
||||
|
||||
# do not update price list
|
||||
frappe.db.set_value("Stock Settings", None, "auto_insert_price_list_rate_if_missing", 0)
|
||||
|
||||
@ -1659,7 +1679,9 @@ def make_sales_order(**args):
|
||||
"warehouse": args.warehouse,
|
||||
"qty": args.qty or 10,
|
||||
"uom": args.uom or None,
|
||||
"rate": args.rate or 100,
|
||||
"price_list_rate": args.price_list_rate or None,
|
||||
"discount_percentage": args.discount_percentage or None,
|
||||
"rate": args.rate or (None if args.price_list_rate else 100),
|
||||
"against_blanket_order": args.against_blanket_order,
|
||||
},
|
||||
)
|
||||
|
@ -179,7 +179,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-14 16:01:29.405642",
|
||||
"modified": "2022-05-31 19:39:48.398738",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Selling Settings",
|
||||
@ -193,6 +193,15 @@
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
|
@ -27,7 +27,7 @@ class SellingSettings(Document):
|
||||
]:
|
||||
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(
|
||||
"Customer",
|
||||
|
@ -1 +0,0 @@
|
||||
Tool to set numbering (naming) series for various DocTypes.
|
@ -1,58 +0,0 @@
|
||||
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
|
||||
frappe.ui.form.on("Naming Series", {
|
||||
onload: function(frm) {
|
||||
frm.events.get_doc_and_prefix(frm);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.disable_save();
|
||||
},
|
||||
|
||||
get_doc_and_prefix: function(frm) {
|
||||
frappe.call({
|
||||
method: "get_transactions",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.set_df_property("select_doc_for_series", "options", r.message.transactions);
|
||||
frm.set_df_property("prefix", "options", r.message.prefixes);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
select_doc_for_series: function(frm) {
|
||||
frm.set_value("user_must_always_select", 0);
|
||||
frappe.call({
|
||||
method: "get_options",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.set_value("set_options", r.message);
|
||||
if(r.message && r.message.split('\n')[0]=='')
|
||||
frm.set_value('user_must_always_select', 1);
|
||||
frm.refresh();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
prefix: function(frm) {
|
||||
frappe.call({
|
||||
method: "get_current",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.refresh_field("current_value");
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
update: function(frm) {
|
||||
frappe.call({
|
||||
method: "update_series",
|
||||
doc: frm.doc,
|
||||
callback: function(r) {
|
||||
frm.events.get_doc_and_prefix(frm);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
@ -1,360 +0,0 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2013-01-25 11:35:08",
|
||||
"custom": 0,
|
||||
"description": "Set prefix for numbering series on your transactions",
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 0,
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Set prefix for numbering series on your transactions",
|
||||
"fieldname": "setup_series",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Setup Series",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "select_doc_for_series",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Select Transaction",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "select_doc_for_series",
|
||||
"fieldname": "help_html",
|
||||
"fieldtype": "HTML",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Help HTML",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "<div class=\"well\">\nEdit list of Series in the box below. Rules:\n<ul>\n<li>Each Series Prefix on a new line.</li>\n<li>Allowed special characters are \"/\" and \"-\"</li>\n<li>Optionally, set the number of digits in the series using dot (.) followed by hashes (#). For example, \".####\" means that the series will have four digits. Default is five digits.</li>\n</ul>\nExamples:<br>\nINV-<br>\nINV-10-<br>\nINVK-<br>\nINV-.####<br>\n</div>",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "select_doc_for_series",
|
||||
"fieldname": "set_options",
|
||||
"fieldtype": "Text",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Series List for this Transaction",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "select_doc_for_series",
|
||||
"description": "Check this if you want to force the user to select a series before saving. There will be no default if you check this.",
|
||||
"fieldname": "user_must_always_select",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "User must always select",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"depends_on": "select_doc_for_series",
|
||||
"fieldname": "update",
|
||||
"fieldtype": "Button",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Update",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "Change the starting / current sequence number of an existing series.",
|
||||
"fieldname": "update_series",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Update Series",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "prefix",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Prefix",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"description": "This is the number of the last created transaction with this prefix",
|
||||
"fieldname": "current_value",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Current Value",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "update_series_start",
|
||||
"fieldtype": "Button",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Update Series Number",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "update_series_start",
|
||||
"permlevel": 0,
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 1,
|
||||
"icon": "fa fa-sort-by-order",
|
||||
"idx": 1,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 1,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2017-08-17 03:41:37.685910",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Naming Series",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"apply_user_permissions": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 0,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 0,
|
||||
"role": "System Manager",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 0,
|
||||
"read_only": 1,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"track_changes": 0,
|
||||
"track_seen": 0
|
||||
}
|
@ -1,274 +0,0 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint, throw
|
||||
from frappe.core.doctype.doctype.doctype import validate_series
|
||||
from frappe.model.document import Document
|
||||
from frappe.model.naming import parse_naming_series
|
||||
from frappe.permissions import get_doctypes_with_read
|
||||
from frappe.utils import cint, cstr
|
||||
|
||||
|
||||
class NamingSeriesNotSetError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
class NamingSeries(Document):
|
||||
@frappe.whitelist()
|
||||
def get_transactions(self, arg=None):
|
||||
doctypes = list(
|
||||
set(
|
||||
frappe.db.sql_list(
|
||||
"""select parent
|
||||
from `tabDocField` df where fieldname='naming_series'"""
|
||||
)
|
||||
+ frappe.db.sql_list(
|
||||
"""select dt from `tabCustom Field`
|
||||
where fieldname='naming_series'"""
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
doctypes = list(set(get_doctypes_with_read()).intersection(set(doctypes)))
|
||||
prefixes = ""
|
||||
for d in doctypes:
|
||||
options = ""
|
||||
try:
|
||||
options = self.get_options(d)
|
||||
except frappe.DoesNotExistError:
|
||||
frappe.msgprint(_("Unable to find DocType {0}").format(d))
|
||||
# frappe.pass_does_not_exist_error()
|
||||
continue
|
||||
|
||||
if options:
|
||||
prefixes = prefixes + "\n" + options
|
||||
prefixes.replace("\n\n", "\n")
|
||||
prefixes = prefixes.split("\n")
|
||||
|
||||
custom_prefixes = frappe.get_all(
|
||||
"DocType",
|
||||
fields=["autoname"],
|
||||
filters={
|
||||
"name": ("not in", doctypes),
|
||||
"autoname": ("like", "%.#%"),
|
||||
"module": ("not in", ["Core"]),
|
||||
},
|
||||
)
|
||||
if custom_prefixes:
|
||||
prefixes = prefixes + [d.autoname.rsplit(".", 1)[0] for d in custom_prefixes]
|
||||
|
||||
prefixes = "\n".join(sorted(prefixes))
|
||||
|
||||
return {"transactions": "\n".join([""] + sorted(doctypes)), "prefixes": prefixes}
|
||||
|
||||
def scrub_options_list(self, ol):
|
||||
options = list(filter(lambda x: x, [cstr(n).strip() for n in ol]))
|
||||
return options
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_series(self, arg=None):
|
||||
"""update series list"""
|
||||
self.validate_series_set()
|
||||
self.check_duplicate()
|
||||
series_list = self.set_options.split("\n")
|
||||
|
||||
# set in doctype
|
||||
self.set_series_for(self.select_doc_for_series, series_list)
|
||||
|
||||
# create series
|
||||
map(self.insert_series, [d.split(".")[0] for d in series_list if d.strip()])
|
||||
|
||||
msgprint(_("Series Updated"))
|
||||
|
||||
return self.get_transactions()
|
||||
|
||||
def validate_series_set(self):
|
||||
if self.select_doc_for_series and not self.set_options:
|
||||
frappe.throw(_("Please set the series to be used."))
|
||||
|
||||
def set_series_for(self, doctype, ol):
|
||||
options = self.scrub_options_list(ol)
|
||||
|
||||
# validate names
|
||||
for i in options:
|
||||
self.validate_series_name(i)
|
||||
|
||||
if options and self.user_must_always_select:
|
||||
options = [""] + options
|
||||
|
||||
default = options[0] if options else ""
|
||||
|
||||
# update in property setter
|
||||
prop_dict = {"options": "\n".join(options), "default": default}
|
||||
|
||||
for prop in prop_dict:
|
||||
ps_exists = frappe.db.get_value(
|
||||
"Property Setter", {"field_name": "naming_series", "doc_type": doctype, "property": prop}
|
||||
)
|
||||
|
||||
if ps_exists:
|
||||
ps = frappe.get_doc("Property Setter", ps_exists)
|
||||
ps.value = prop_dict[prop]
|
||||
ps.save()
|
||||
else:
|
||||
ps = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Property Setter",
|
||||
"doctype_or_field": "DocField",
|
||||
"doc_type": doctype,
|
||||
"field_name": "naming_series",
|
||||
"property": prop,
|
||||
"value": prop_dict[prop],
|
||||
"property_type": "Text",
|
||||
"__islocal": 1,
|
||||
}
|
||||
)
|
||||
ps.save()
|
||||
|
||||
self.set_options = "\n".join(options)
|
||||
|
||||
frappe.clear_cache(doctype=doctype)
|
||||
|
||||
def check_duplicate(self):
|
||||
parent = list(
|
||||
set(
|
||||
frappe.db.sql_list(
|
||||
"""select dt.name
|
||||
from `tabDocField` df, `tabDocType` dt
|
||||
where dt.name = df.parent and df.fieldname='naming_series' and dt.name != %s""",
|
||||
self.select_doc_for_series,
|
||||
)
|
||||
+ frappe.db.sql_list(
|
||||
"""select dt.name
|
||||
from `tabCustom Field` df, `tabDocType` dt
|
||||
where dt.name = df.dt and df.fieldname='naming_series' and dt.name != %s""",
|
||||
self.select_doc_for_series,
|
||||
)
|
||||
)
|
||||
)
|
||||
sr = [[frappe.get_meta(p).get_field("naming_series").options, p] for p in parent]
|
||||
dt = frappe.get_doc("DocType", self.select_doc_for_series)
|
||||
options = self.scrub_options_list(self.set_options.split("\n"))
|
||||
for series in options:
|
||||
validate_series(dt, series)
|
||||
for i in sr:
|
||||
if i[0]:
|
||||
existing_series = [d.split(".")[0] for d in i[0].split("\n")]
|
||||
if series.split(".")[0] in existing_series:
|
||||
frappe.throw(_("Series {0} already used in {1}").format(series, i[1]))
|
||||
|
||||
def validate_series_name(self, n):
|
||||
import re
|
||||
|
||||
if not re.match(r"^[\w\- \/.#{}]+$", n, re.UNICODE):
|
||||
throw(
|
||||
_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series')
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_options(self, arg=None):
|
||||
if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"):
|
||||
return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_current(self, arg=None):
|
||||
"""get series current"""
|
||||
if self.prefix:
|
||||
prefix = self.parse_naming_series()
|
||||
self.current_value = frappe.db.get_value("Series", prefix, "current", order_by="name")
|
||||
|
||||
def insert_series(self, series):
|
||||
"""insert series if missing"""
|
||||
if frappe.db.get_value("Series", series, "name", order_by="name") == None:
|
||||
frappe.db.sql("insert into tabSeries (name, current) values (%s, 0)", (series))
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_series_start(self):
|
||||
if self.prefix:
|
||||
prefix = self.parse_naming_series()
|
||||
self.insert_series(prefix)
|
||||
frappe.db.sql(
|
||||
"update `tabSeries` set current = %s where name = %s", (cint(self.current_value), prefix)
|
||||
)
|
||||
msgprint(_("Series Updated Successfully"))
|
||||
else:
|
||||
msgprint(_("Please select prefix first"))
|
||||
|
||||
def parse_naming_series(self):
|
||||
parts = self.prefix.split(".")
|
||||
|
||||
# Remove ### from the end of series
|
||||
if parts[-1] == "#" * len(parts[-1]):
|
||||
del parts[-1]
|
||||
|
||||
prefix = parse_naming_series(parts)
|
||||
return prefix
|
||||
|
||||
|
||||
def set_by_naming_series(
|
||||
doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1
|
||||
):
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
if naming_series:
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
|
||||
# set values for mandatory
|
||||
try:
|
||||
frappe.db.sql(
|
||||
"""update `tab{doctype}` set naming_series={s} where
|
||||
ifnull(naming_series, '')=''""".format(
|
||||
doctype=doctype, s="%s"
|
||||
),
|
||||
get_default_naming_series(doctype),
|
||||
)
|
||||
except NamingSeriesNotSetError:
|
||||
pass
|
||||
|
||||
if hide_name_field:
|
||||
make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False)
|
||||
make_property_setter(
|
||||
doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
else:
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
|
||||
if hide_name_field:
|
||||
make_property_setter(
|
||||
doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False)
|
||||
|
||||
# set values for mandatory
|
||||
frappe.db.sql(
|
||||
"""update `tab{doctype}` set `{fieldname}`=`name` where
|
||||
ifnull({fieldname}, '')=''""".format(
|
||||
doctype=doctype, fieldname=fieldname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_default_naming_series(doctype):
|
||||
naming_series = frappe.get_meta(doctype).get_field("naming_series").options or ""
|
||||
naming_series = naming_series.split("\n")
|
||||
out = naming_series[0] or (naming_series[1] if len(naming_series) > 1 else None)
|
||||
|
||||
if not out:
|
||||
frappe.throw(
|
||||
_("Please set Naming Series for {0} via Setup > Settings > Naming Series").format(doctype),
|
||||
NamingSeriesNotSetError,
|
||||
)
|
||||
else:
|
||||
return out
|
@ -86,20 +86,29 @@ def get_batch_naming_series():
|
||||
class Batch(Document):
|
||||
def autoname(self):
|
||||
"""Generate random ID for batch if not specified"""
|
||||
if not self.batch_id:
|
||||
create_new_batch, batch_number_series = frappe.db.get_value(
|
||||
"Item", self.item, ["create_new_batch", "batch_number_series"]
|
||||
)
|
||||
|
||||
if create_new_batch:
|
||||
if batch_number_series:
|
||||
self.batch_id = make_autoname(batch_number_series, doc=self)
|
||||
elif batch_uses_naming_series():
|
||||
self.batch_id = self.get_name_from_naming_series()
|
||||
else:
|
||||
self.batch_id = get_name_from_hash()
|
||||
if self.batch_id:
|
||||
self.name = self.batch_id
|
||||
return
|
||||
|
||||
create_new_batch, batch_number_series = frappe.db.get_value(
|
||||
"Item", self.item, ["create_new_batch", "batch_number_series"]
|
||||
)
|
||||
|
||||
if not create_new_batch:
|
||||
frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError)
|
||||
|
||||
while not self.batch_id:
|
||||
if batch_number_series:
|
||||
self.batch_id = make_autoname(batch_number_series, doc=self)
|
||||
elif batch_uses_naming_series():
|
||||
self.batch_id = self.get_name_from_naming_series()
|
||||
else:
|
||||
frappe.throw(_("Batch ID is mandatory"), frappe.MandatoryError)
|
||||
self.batch_id = get_name_from_hash()
|
||||
|
||||
# User might have manually created a batch with next number
|
||||
if frappe.db.exists("Batch", self.batch_id):
|
||||
self.batch_id = None
|
||||
|
||||
self.name = self.batch_id
|
||||
|
||||
|
@ -11,6 +11,8 @@ from frappe.utils.data import add_to_date, getdate
|
||||
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||
create_stock_reconciliation,
|
||||
@ -27,7 +29,7 @@ class TestBatch(FrappeTestCase):
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_batch_item(cls, item_name):
|
||||
def make_batch_item(cls, item_name=None):
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
|
||||
if not frappe.db.exists(item_name):
|
||||
@ -245,7 +247,7 @@ class TestBatch(FrappeTestCase):
|
||||
if not use_naming_series:
|
||||
frappe.set_value("Stock Settings", "Stock Settings", "use_naming_series", 0)
|
||||
|
||||
def make_new_batch(self, item_name, batch_id=None, do_not_insert=0):
|
||||
def make_new_batch(self, item_name=None, batch_id=None, do_not_insert=0):
|
||||
batch = frappe.new_doc("Batch")
|
||||
item = self.make_batch_item(item_name)
|
||||
batch.item = item.name
|
||||
@ -407,6 +409,26 @@ class TestBatch(FrappeTestCase):
|
||||
|
||||
self.assertEqual(getdate(batch.expiry_date), getdate(expiry_date))
|
||||
|
||||
def test_autocreation_of_batches(self):
|
||||
"""
|
||||
Test if auto created Serial No excludes existing serial numbers
|
||||
"""
|
||||
item_code = make_item(
|
||||
properties={
|
||||
"has_batch_no": 1,
|
||||
"batch_number_series": "BATCHEXISTING.###",
|
||||
"create_new_batch": 1,
|
||||
}
|
||||
).name
|
||||
|
||||
manually_created_batch = self.make_new_batch(item_code, batch_id="BATCHEXISTING001").name
|
||||
|
||||
pr_1 = make_purchase_receipt(item_code=item_code, qty=1, batch_no=manually_created_batch)
|
||||
pr_2 = make_purchase_receipt(item_code=item_code, qty=1)
|
||||
|
||||
self.assertNotEqual(pr_1.items[0].batch_no, pr_2.items[0].batch_no)
|
||||
self.assertEqual("BATCHEXISTING002", pr_2.items[0].batch_no)
|
||||
|
||||
|
||||
def create_batch(item_code, rate, create_item_price_for_batch):
|
||||
pi = make_purchase_invoice(
|
||||
|
@ -6,7 +6,8 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"barcode",
|
||||
"barcode_type"
|
||||
"barcode_type",
|
||||
"uom"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -24,11 +25,18 @@
|
||||
"in_list_view": 1,
|
||||
"label": "Barcode Type",
|
||||
"options": "\nEAN\nUPC-A"
|
||||
},
|
||||
{
|
||||
"fieldname": "uom",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "UOM",
|
||||
"options": "UOM"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-01 05:54:27.314030",
|
||||
"modified": "2022-06-01 06:24:33.969534",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item Barcode",
|
||||
|
@ -108,8 +108,6 @@
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
"bill_no",
|
||||
"bill_date",
|
||||
"more_info",
|
||||
"status",
|
||||
"amended_from",
|
||||
@ -867,24 +865,6 @@
|
||||
"oldfieldname": "terms",
|
||||
"oldfieldtype": "Text Editor"
|
||||
},
|
||||
{
|
||||
"fieldname": "bill_no",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Bill No",
|
||||
"oldfieldname": "bill_no",
|
||||
"oldfieldtype": "Data",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "bill_date",
|
||||
"fieldtype": "Date",
|
||||
"hidden": 1,
|
||||
"label": "Bill Date",
|
||||
"oldfieldname": "bill_date",
|
||||
"oldfieldtype": "Date",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "more_info",
|
||||
@ -1168,7 +1148,7 @@
|
||||
"idx": 261,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2022-04-26 13:41:32.625197",
|
||||
"modified": "2022-05-27 15:59:18.550583",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Purchase Receipt",
|
||||
|
@ -26,7 +26,7 @@ class StockSettings(Document):
|
||||
]:
|
||||
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(
|
||||
"Item",
|
||||
|
@ -1,88 +1,97 @@
|
||||
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
frappe.ui.form.on("Warehouse", {
|
||||
onload: function(frm) {
|
||||
frm.set_query("default_in_transit_warehouse", function() {
|
||||
setup: function (frm) {
|
||||
frm.set_query("default_in_transit_warehouse", function (doc) {
|
||||
return {
|
||||
filters:{
|
||||
'warehouse_type' : 'Transit',
|
||||
'is_group': 0,
|
||||
'company': frm.doc.company
|
||||
}
|
||||
filters: {
|
||||
warehouse_type: "Transit",
|
||||
is_group: 0,
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("parent_warehouse", function () {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("account", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
is_group: 0,
|
||||
account_type: "Stock",
|
||||
company: doc.company,
|
||||
},
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.toggle_display('warehouse_name', frm.doc.__islocal);
|
||||
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
|
||||
refresh: function (frm) {
|
||||
frm.toggle_display("warehouse_name", frm.doc.__islocal);
|
||||
frm.toggle_display(
|
||||
["address_html", "contact_html"],
|
||||
!frm.doc.__islocal
|
||||
);
|
||||
|
||||
|
||||
if(!frm.doc.__islocal) {
|
||||
if (!frm.doc.__islocal) {
|
||||
frappe.contacts.render_address_and_contact(frm);
|
||||
|
||||
} else {
|
||||
frappe.contacts.clear_address_and_contact(frm);
|
||||
}
|
||||
|
||||
frm.add_custom_button(__("Stock Balance"), function() {
|
||||
frappe.set_route("query-report", "Stock Balance", {"warehouse": frm.doc.name});
|
||||
frm.add_custom_button(__("Stock Balance"), function () {
|
||||
frappe.set_route("query-report", "Stock Balance", {
|
||||
warehouse: frm.doc.name,
|
||||
});
|
||||
});
|
||||
|
||||
if (cint(frm.doc.is_group) == 1) {
|
||||
frm.add_custom_button(__('Group to Non-Group'),
|
||||
function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default')
|
||||
} else if (cint(frm.doc.is_group) == 0) {
|
||||
if(frm.doc.__onload && frm.doc.__onload.account) {
|
||||
frm.add_custom_button(__("General Ledger"), function() {
|
||||
frm.add_custom_button(
|
||||
frm.doc.is_group
|
||||
? __("Convert to Ledger", null, "Warehouse")
|
||||
: __("Convert to Group", null, "Warehouse"),
|
||||
function () {
|
||||
convert_to_group_or_ledger(frm);
|
||||
},
|
||||
);
|
||||
|
||||
if (!frm.doc.is_group && frm.doc.__onload && frm.doc.__onload.account) {
|
||||
frm.add_custom_button(
|
||||
__("General Ledger", null, "Warehouse"),
|
||||
function () {
|
||||
frappe.route_options = {
|
||||
"account": frm.doc.__onload.account,
|
||||
"company": frm.doc.company
|
||||
}
|
||||
account: frm.doc.__onload.account,
|
||||
company: frm.doc.company,
|
||||
};
|
||||
frappe.set_route("query-report", "General Ledger");
|
||||
});
|
||||
}
|
||||
|
||||
frm.add_custom_button(__('Non-Group to Group'),
|
||||
function() { convert_to_group_or_ledger(frm); }, 'fa fa-retweet', 'btn-default')
|
||||
}
|
||||
|
||||
frm.toggle_enable(['is_group', 'company'], false);
|
||||
|
||||
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Warehouse'};
|
||||
|
||||
frm.fields_dict['parent_warehouse'].get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"is_group": 1,
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
frm.fields_dict['account'].get_query = function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"is_group": 0,
|
||||
"account_type": "Stock",
|
||||
"company": frm.doc.company
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
frm.toggle_enable(["is_group", "company"], false);
|
||||
|
||||
frappe.dynamic_link = {
|
||||
doc: frm.doc,
|
||||
fieldname: "name",
|
||||
doctype: "Warehouse",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
function convert_to_group_or_ledger(frm){
|
||||
function convert_to_group_or_ledger(frm) {
|
||||
frappe.call({
|
||||
method:"erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger",
|
||||
method: "erpnext.stock.doctype.warehouse.warehouse.convert_to_group_or_ledger",
|
||||
args: {
|
||||
docname: frm.doc.name,
|
||||
is_group: frm.doc.is_group
|
||||
is_group: frm.doc.is_group,
|
||||
},
|
||||
callback: function(){
|
||||
callback: function () {
|
||||
frm.refresh();
|
||||
}
|
||||
|
||||
})
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -353,6 +353,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"has_batch_no": item.has_batch_no,
|
||||
"batch_no": args.get("batch_no"),
|
||||
"uom": args.uom,
|
||||
"stock_uom": item.stock_uom,
|
||||
"min_order_qty": flt(item.min_order_qty) if args.doctype == "Material Request" else "",
|
||||
"qty": flt(args.qty) or 1.0,
|
||||
"stock_qty": flt(args.qty) or 1.0,
|
||||
@ -365,7 +366,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"net_rate": 0.0,
|
||||
"net_amount": 0.0,
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": 0.0,
|
||||
"discount_amount": flt(args.discount_amount) or 0.0,
|
||||
"supplier": get_default_supplier(args, item_defaults, item_group_defaults, brand_defaults),
|
||||
"update_stock": args.get("update_stock")
|
||||
if args.get("doctype") in ["Sales Invoice", "Purchase Invoice"]
|
||||
@ -823,7 +824,9 @@ def insert_item_price(args):
|
||||
):
|
||||
if frappe.has_permission("Item Price", "write"):
|
||||
price_list_rate = (
|
||||
args.rate / args.get("conversion_factor") if args.get("conversion_factor") else args.rate
|
||||
(args.rate + args.discount_amount) / args.get("conversion_factor")
|
||||
if args.get("conversion_factor")
|
||||
else (args.rate + args.discount_amount)
|
||||
)
|
||||
|
||||
item_price = frappe.db.get_value(
|
||||
@ -849,6 +852,7 @@ def insert_item_price(args):
|
||||
"item_code": args.item_code,
|
||||
"currency": args.currency,
|
||||
"price_list_rate": price_list_rate,
|
||||
"uom": args.stock_uom,
|
||||
}
|
||||
)
|
||||
item_price.insert()
|
||||
|
@ -558,7 +558,7 @@ def scan_barcode(search_value: str) -> Dict[str, Optional[str]]:
|
||||
barcode_data = frappe.db.get_value(
|
||||
"Item Barcode",
|
||||
{"barcode": search_value},
|
||||
["barcode", "parent as item_code"],
|
||||
["barcode", "parent as item_code", "uom"],
|
||||
as_dict=True,
|
||||
)
|
||||
if barcode_data:
|
||||
|
@ -1,19 +0,0 @@
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.contacts.address_and_contact import filter_dynamic_link_doctypes
|
||||
|
||||
|
||||
class TestSearch(unittest.TestCase):
|
||||
# Search for the word "cond", part of the word "conduire" (Lead) in french.
|
||||
def test_contact_search_in_foreign_language(self):
|
||||
try:
|
||||
frappe.local.lang_full_dict = None # reset cached translations
|
||||
frappe.local.lang = "fr"
|
||||
output = filter_dynamic_link_doctypes(
|
||||
"DocType", "cond", "name", 0, 20, {"fieldtype": "HTML", "fieldname": "contact_html"}
|
||||
)
|
||||
result = [["found" for x in y if x == "Lead"] for y in output]
|
||||
self.assertTrue(["found"] in result)
|
||||
finally:
|
||||
frappe.local.lang = "en"
|
@ -1178,7 +1178,7 @@ Group by Party,Gruppieren nach Partei,
|
||||
Group by Voucher,Gruppieren nach Beleg,
|
||||
Group by Voucher (Consolidated),Gruppieren nach Beleg (konsolidiert),
|
||||
Group node warehouse is not allowed to select for transactions,Gruppenknoten Lager ist nicht für Transaktionen zu wählen erlaubt,
|
||||
Group to Non-Group,Gruppe an konzernfremde,
|
||||
Convert to Ledger,In Lagerbuch umwandeln,Warehouse
|
||||
Group your students in batches,Gruppieren Sie Ihre Schüler in den Reihen,
|
||||
Groups,Gruppen,
|
||||
Guardian1 Email ID,Guardian1 E-Mail-ID,
|
||||
@ -1735,7 +1735,6 @@ Non GST Inward Supplies,Nicht GST Inward Supplies,
|
||||
Non Profit,Gemeinnützig,
|
||||
Non Profit (beta),Non-Profit (Beta),
|
||||
Non-GST outward supplies,Nicht-GST-Lieferungen nach außen,
|
||||
Non-Group to Group,Non-Group-Gruppe,
|
||||
None,Keiner,
|
||||
None of the items have any change in quantity or value.,Keiner der Artikel hat irgendeine Änderung bei Mengen oder Kosten.,
|
||||
Nos,Stk,
|
||||
|
Can't render this file because it is too large.
|
@ -175,7 +175,7 @@ Airline,Compagnie aérienne,
|
||||
All Accounts,Tous les comptes,
|
||||
All Addresses.,Toutes les adresses.,
|
||||
All Assessment Groups,Tous les Groupes d'Évaluation,
|
||||
All BOMs,Toutes les LDM,
|
||||
All BOMs,Toutes les nomenclatures,
|
||||
All Contacts.,Tous les contacts.,
|
||||
All Customer Groups,Tous les Groupes Client,
|
||||
All Day,Toute la Journée,
|
||||
@ -330,16 +330,16 @@ Avg Daily Outgoing,Moy Quotidienne Sortante,
|
||||
Avg. Buying Price List Rate,Moyenne de la liste de prix d'achat,
|
||||
Avg. Selling Price List Rate,Prix moyen de la liste de prix de vente,
|
||||
Avg. Selling Rate,Moy. Taux de vente,
|
||||
BOM,LDM (Liste de Matériaux),
|
||||
BOM Browser,Explorateur LDM,
|
||||
BOM No,N° LDM,
|
||||
BOM Rate,Taux LDM,
|
||||
BOM Stock Report,Rapport de Stock de LDM,
|
||||
BOM and Manufacturing Quantity are required,LDM et quantité de production sont nécessaires,
|
||||
BOM does not contain any stock item,LDM ne contient aucun article en stock,
|
||||
BOM {0} does not belong to Item {1},LDM {0} n’appartient pas à l'article {1},
|
||||
BOM {0} must be active,LDM {0} doit être active,
|
||||
BOM {0} must be submitted,LDM {0} doit être soumise,
|
||||
BOM,Nomenclature,
|
||||
BOM Browser,Explorateur Nomenclature,
|
||||
BOM No,N° Nomenclature,
|
||||
BOM Rate,Valeur nomenclature,
|
||||
BOM Stock Report,Rapport de Stock des nomenclatures,
|
||||
BOM and Manufacturing Quantity are required,Nomenclature et quantité de production sont nécessaires,
|
||||
BOM does not contain any stock item,Nomenclature ne contient aucun article en stock,
|
||||
BOM {0} does not belong to Item {1},Nomenclature {0} n’appartient pas à l'article {1},
|
||||
BOM {0} must be active,Nomenclature {0} doit être active,
|
||||
BOM {0} must be submitted,Nomenclature {0} doit être soumise,
|
||||
Balance,Solde,
|
||||
Balance (Dr - Cr),Balance (Dr - Cr),
|
||||
Balance ({0}),Solde ({0}),
|
||||
@ -386,8 +386,8 @@ Beginner,Débutant,
|
||||
Bill,Facture,
|
||||
Bill Date,Date de la Facture,
|
||||
Bill No,Numéro de facture,
|
||||
Bill of Materials,Liste de Matériaux,
|
||||
Bill of Materials (BOM),Liste de Matériaux (LDM),
|
||||
Bill of Materials,Nomenclatures,
|
||||
Bill of Materials (BOM),Nomenclature,
|
||||
Billable Hours,Heures facturables,
|
||||
Billed,Facturé,
|
||||
Billed Amount,Montant facturé,
|
||||
@ -404,14 +404,14 @@ Birthday Reminder,Rappel d'anniversaire,
|
||||
Black,Noir,
|
||||
Blanket Orders from Costumers.,Commandes provisoires de clients.,
|
||||
Block Invoice,Bloquer la facture,
|
||||
Boms,Listes de Matériaux,
|
||||
Boms,Nomenclatures,
|
||||
Bonus Payment Date cannot be a past date,La date de paiement du bonus ne peut pas être une date passée,
|
||||
Both Trial Period Start Date and Trial Period End Date must be set,La date de début de la période d'essai et la date de fin de la période d'essai doivent être définies,
|
||||
Both Warehouse must belong to same Company,Les deux Entrepôt doivent appartenir à la même Société,
|
||||
Branch,Branche,
|
||||
Broadcasting,Radio/Télévision,
|
||||
Brokerage,Courtage,
|
||||
Browse BOM,Parcourir la LDM,
|
||||
Browse BOM,Parcourir la nomenclature,
|
||||
Budget Against,Budget Pour,
|
||||
Budget List,Liste budgétaire,
|
||||
Budget Variance Report,Rapport d’Écarts de Budget,
|
||||
@ -467,7 +467,7 @@ Cannot convert Cost Center to ledger as it has child nodes,Conversion impossible
|
||||
Cannot covert to Group because Account Type is selected.,Conversion impossible en Groupe car le Type de Compte est sélectionné.,
|
||||
Cannot create Retention Bonus for left Employees,Impossible de créer une prime de fidélisation pour les employés ayant quitté l'entreprise,
|
||||
Cannot create a Delivery Trip from Draft documents.,Impossible de créer un voyage de livraison à partir de documents brouillons.,
|
||||
Cannot deactivate or cancel BOM as it is linked with other BOMs,Désactivation ou annulation de la LDM impossible car elle est liée avec d'autres LDMs,
|
||||
Cannot deactivate or cancel BOM as it is linked with other BOMs,Désactivation ou annulation de la nomenclature impossible car elle est liée avec d'autres nomenclatures,
|
||||
"Cannot declare as lost, because Quotation has been made.","Impossible de déclarer comme perdu, parce que le Devis a été fait.",
|
||||
Cannot deduct when category is for 'Valuation' or 'Valuation and Total',Déduction impossible lorsque la catégorie est pour 'Évaluation' ou 'Vaulation et Total',
|
||||
Cannot deduct when category is for 'Valuation' or 'Vaulation and Total',Vous ne pouvez pas déduire lorsqu'une catégorie est pour 'Évaluation' ou 'Évaluation et Total',
|
||||
@ -722,7 +722,7 @@ Currency of the price list {0} must be {1} or {2},La devise de la liste de prix
|
||||
Currency should be same as Price List Currency: {0},La devise doit être la même que la devise de la liste de prix: {0},
|
||||
Current,Actuel,
|
||||
Current Assets,Actifs Actuels,
|
||||
Current BOM and New BOM can not be same,La LDM actuelle et la nouvelle LDM ne peuvent être pareilles,
|
||||
Current BOM and New BOM can not be same,La nomenclature actuelle et la nouvelle nomenclature ne peuvent être pareilles,
|
||||
Current Job Openings,Offres d'Emploi Actuelles,
|
||||
Current Liabilities,Dettes Actuelles,
|
||||
Current Qty,Qté actuelle,
|
||||
@ -780,9 +780,9 @@ Debtors ({0}),Débiteurs ({0}),
|
||||
Declare Lost,Déclarer perdu,
|
||||
Deduction,Déduction,
|
||||
Default Activity Cost exists for Activity Type - {0},Un Coût d’Activité par défault existe pour le Type d’Activité {0},
|
||||
Default BOM ({0}) must be active for this item or its template,LDM par défaut ({0}) doit être actif pour ce produit ou son modèle,
|
||||
Default BOM for {0} not found,LDM par défaut {0} introuvable,
|
||||
Default BOM not found for Item {0} and Project {1},La LDM par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1},
|
||||
Default BOM ({0}) must be active for this item or its template,Nomenclature par défaut ({0}) doit être actif pour ce produit ou son modèle,
|
||||
Default BOM for {0} not found,Nomenclature par défaut {0} introuvable,
|
||||
Default BOM not found for Item {0} and Project {1},La nomenclature par défaut n'a pas été trouvée pour l'Article {0} et le Projet {1},
|
||||
Default Letter Head,En-Tête de Courrier par Défaut,
|
||||
Default Tax Template,Modèle de Taxes par Défaut,
|
||||
Default Unit of Measure for Item {0} cannot be changed directly because you have already made some transaction(s) with another UOM. You will need to create a new Item to use a different Default UOM.,L’Unité de Mesure par Défaut pour l’Article {0} ne peut pas être modifiée directement parce que vous avez déjà fait une (des) transaction (s) avec une autre unité de mesure. Vous devez créer un nouvel article pour utiliser une UDM par défaut différente.,
|
||||
@ -1023,7 +1023,7 @@ Fees,Honoraires,
|
||||
Female,Féminin,
|
||||
Fetch Data,Récupérer des données,
|
||||
Fetch Subscription Updates,Vérifier les mises à jour des abonnements,
|
||||
Fetch exploded BOM (including sub-assemblies),Récupérer la LDM éclatée (y compris les sous-ensembles),
|
||||
Fetch exploded BOM (including sub-assemblies),Récupérer la nomenclature éclatée (y compris les sous-ensembles),
|
||||
Fetching records......,Récupération des enregistrements ......,
|
||||
Field Name,Nom du Champ,
|
||||
Fieldname,Nom du Champ,
|
||||
@ -1135,7 +1135,7 @@ Get Employees,Obtenir des employés,
|
||||
Get Invocies,Obtenir des invocies,
|
||||
Get Invoices,Obtenir des factures,
|
||||
Get Invoices based on Filters,Obtenir les factures en fonction des filtres,
|
||||
Get Items from BOM,Obtenir les Articles depuis LDM,
|
||||
Get Items from BOM,Obtenir les Articles depuis nomenclature,
|
||||
Get Items from Healthcare Services,Obtenir des articles des services de santé,
|
||||
Get Items from Prescriptions,Obtenir des articles des prescriptions,
|
||||
Get Items from Product Bundle,Obtenir les Articles du Produit Groupé,
|
||||
@ -1425,8 +1425,8 @@ Last Order Date,Date de la dernière commande,
|
||||
Last Purchase Price,Dernier prix d'achat,
|
||||
Last Purchase Rate,Dernier Prix d'Achat,
|
||||
Latest,Dernier,
|
||||
Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les LDMs,
|
||||
Lead,Conduire,
|
||||
Latest price updated in all BOMs,Prix les plus récents mis à jour dans toutes les nomenclatures,
|
||||
Lead,Prospect,
|
||||
Lead Count,Nombre de Prospects,
|
||||
Lead Owner,Responsable du Prospect,
|
||||
Lead Owner cannot be same as the Lead,Le Responsable du Prospect ne peut pas être identique au Prospect,
|
||||
@ -1655,7 +1655,7 @@ Net Total,Total net,
|
||||
Net pay cannot be negative,Salaire Net ne peut pas être négatif,
|
||||
New Account Name,Nouveau Nom de Compte,
|
||||
New Address,Nouvelle adresse,
|
||||
New BOM,Nouvelle LDM,
|
||||
New BOM,Nouvelle nomenclature,
|
||||
New Batch ID (Optional),Nouveau Numéro de Lot (Optionnel),
|
||||
New Batch Qty,Nouvelle Qté de Lot,
|
||||
New Company,Nouvelle Société,
|
||||
@ -1689,7 +1689,7 @@ No Item with Serial No {0},Aucun Article avec le N° de Série {0},
|
||||
No Items available for transfer,Aucun article disponible pour le transfert,
|
||||
No Items selected for transfer,Aucun article sélectionné pour le transfert,
|
||||
No Items to pack,Pas d’Articles à emballer,
|
||||
No Items with Bill of Materials to Manufacture,Aucun Article avec une Liste de Matériel à Produire,
|
||||
No Items with Bill of Materials to Manufacture,Aucun Article avec une nomenclature à Produire,
|
||||
No Items with Bill of Materials.,Aucun article avec nomenclature.,
|
||||
No Permission,Aucune autorisation,
|
||||
No Remarks,Aucune Remarque,
|
||||
@ -1777,7 +1777,7 @@ Online Auctions,Enchères en ligne,
|
||||
Only Leave Applications with status 'Approved' and 'Rejected' can be submitted,Seules les Demandes de Congés avec le statut 'Appouvée' ou 'Rejetée' peuvent être soumises,
|
||||
"Only the Student Applicant with the status ""Approved"" will be selected in the table below.",Seul les candidatures étudiantes avec le statut «Approuvé» seront sélectionnées dans le tableau ci-dessous.,
|
||||
Only users with {0} role can register on Marketplace,Seuls les utilisateurs ayant le rôle {0} peuvent s'inscrire sur Marketplace,
|
||||
Open BOM {0},Ouvrir LDM {0},
|
||||
Open BOM {0},Ouvrir nomenclature {0},
|
||||
Open Item {0},Ouvrir l'Article {0},
|
||||
Open Notifications,Notifications ouvertes,
|
||||
Open Orders,Commandes ouvertes,
|
||||
@ -2015,9 +2015,9 @@ Please save the patient first,Veuillez d'abord enregistrer le patient,
|
||||
Please save the report again to rebuild or update,Veuillez enregistrer le rapport à nouveau pour reconstruire ou mettre à jour,
|
||||
"Please select Allocated Amount, Invoice Type and Invoice Number in atleast one row","Veuillez sélectionner le Montant Alloué, le Type de Facture et le Numéro de Facture dans au moins une ligne",
|
||||
Please select Apply Discount On,Veuillez sélectionnez Appliquer Remise Sur,
|
||||
Please select BOM against item {0},Veuillez sélectionner la liste de matériaux (LDM) pour l'article {0},
|
||||
Please select BOM for Item in Row {0},Veuillez sélectionnez une LDM pour l’Article à la Ligne {0},
|
||||
Please select BOM in BOM field for Item {0},Veuillez sélectionner une LDM dans le champ LDM pour l’Article {0},
|
||||
Please select BOM against item {0},Veuillez sélectionner la nomenclature pour l'article {0},
|
||||
Please select BOM for Item in Row {0},Veuillez sélectionnez une nomenclature pour l’Article à la Ligne {0},
|
||||
Please select BOM in BOM field for Item {0},Veuillez sélectionner une nomenclature dans le champ nomenclature pour l’Article {0},
|
||||
Please select Category first,Veuillez d’abord sélectionner une Catégorie,
|
||||
Please select Charge Type first,Veuillez d’abord sélectionner le Type de Facturation,
|
||||
Please select Company,Veuillez sélectionner une Société,
|
||||
@ -2044,7 +2044,7 @@ Please select Qty against item {0},Veuillez sélectionner Qté par rapport à l'
|
||||
Please select Sample Retention Warehouse in Stock Settings first,Veuillez d'abord définir un entrepôt de stockage des échantillons dans les paramètres de stock,
|
||||
Please select Start Date and End Date for Item {0},Veuillez sélectionner la Date de Début et Date de Fin pour l'Article {0},
|
||||
Please select Student Admission which is mandatory for the paid student applicant,Veuillez sélectionner obligatoirement une Admission d'Étudiant pour la candidature étudiante payée,
|
||||
Please select a BOM,Veuillez sélectionner une LDM,
|
||||
Please select a BOM,Veuillez sélectionner une nomenclature,
|
||||
Please select a Batch for Item {0}. Unable to find a single batch that fulfills this requirement,Veuillez sélectionner un Lot pour l'Article {0}. Impossible de trouver un seul lot satisfaisant à cette exigence,
|
||||
Please select a Company,Veuillez sélectionner une Société,
|
||||
Please select a batch,Veuillez sélectionner un lot,
|
||||
@ -2273,8 +2273,8 @@ Quantity to Manufacture must be greater than 0.,La quantité à produire doit ê
|
||||
Quantity to Produce,Quantité à produire,
|
||||
Quantity to Produce can not be less than Zero,La quantité à produire ne peut être inférieure à zéro,
|
||||
Query Options,Options de Requête,
|
||||
Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la LDM. Cela peut prendre quelques minutes.,
|
||||
Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les Listes de Matériaux en file d'attente. Cela peut prendre quelques minutes.,
|
||||
Queued for replacing the BOM. It may take a few minutes.,En file d'attente pour remplacer la nomenclature. Cela peut prendre quelques minutes.,
|
||||
Queued for updating latest price in all Bill of Materials. It may take a few minutes.,Mise à jour des prix les plus récents dans toutes les nomenclatures en file d'attente. Cela peut prendre quelques minutes.,
|
||||
Quick Journal Entry,Écriture Rapide dans le Journal,
|
||||
Quot Count,Compte de Devis,
|
||||
Quot/Lead %,Devis / Prospects %,
|
||||
@ -2354,7 +2354,7 @@ Reorder Level,Niveau de réapprovisionnement,
|
||||
Reorder Qty,Qté de Réapprovisionnement,
|
||||
Repeat Customer Revenue,Revenus de Clients Récurrents,
|
||||
Repeat Customers,Clients Récurrents,
|
||||
Replace BOM and update latest price in all BOMs,Remplacer la LDM et actualiser les prix les plus récents dans toutes les LDMs,
|
||||
Replace BOM and update latest price in all BOMs,Remplacer la nomenclature et actualiser les prix les plus récents dans toutes les nomenclatures,
|
||||
Replied,Répondu,
|
||||
Replies,réponses,
|
||||
Report,Rapport,
|
||||
@ -2466,11 +2466,11 @@ Row {0}: Advance against Supplier must be debit,Ligne {0} : L’Avance du Fourni
|
||||
Row {0}: Allocated amount {1} must be less than or equals to Payment Entry amount {2},Ligne {0} : Le montant alloué {1} doit être inférieur ou égal au montant du Paiement {2},
|
||||
Row {0}: Allocated amount {1} must be less than or equals to invoice outstanding amount {2},Ligne {0} : Le montant alloué {1} doit être inférieur ou égal au montant restant sur la Facture {2},
|
||||
Row {0}: An Reorder entry already exists for this warehouse {1},Ligne {0} : Une écriture de Réapprovisionnement existe déjà pour cet entrepôt {1},
|
||||
Row {0}: Bill of Materials not found for the Item {1},Ligne {0} : Liste de Matériaux non trouvée pour l’Article {1},
|
||||
Row {0}: Bill of Materials not found for the Item {1},Ligne {0} : Nomenclature non trouvée pour l’Article {1},
|
||||
Row {0}: Conversion Factor is mandatory,Ligne {0} : Le Facteur de Conversion est obligatoire,
|
||||
Row {0}: Cost center is required for an item {1},Ligne {0}: le Centre de Coûts est requis pour un article {1},
|
||||
Row {0}: Credit entry can not be linked with a {1},Ligne {0} : L’Écriture de crédit ne peut pas être liée à un {1},
|
||||
Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la LDM #{1} doit être égale à la devise sélectionnée {2},
|
||||
Row {0}: Currency of the BOM #{1} should be equal to the selected currency {2},Ligne {0} : La devise de la nomenclature #{1} doit être égale à la devise sélectionnée {2},
|
||||
Row {0}: Debit entry can not be linked with a {1},Ligne {0} : L’Écriture de Débit ne peut pas être lié à un {1},
|
||||
Row {0}: Depreciation Start Date is required,Ligne {0}: la date de début de l'amortissement est obligatoire,
|
||||
Row {0}: Enter location for the asset item {1},Ligne {0}: entrez la localisation de l'actif {1},
|
||||
@ -2490,7 +2490,7 @@ Row {0}: Please set the Mode of Payment in Payment Schedule,Ligne {0}: Veuillez
|
||||
Row {0}: Please set the correct code on Mode of Payment {1},Ligne {0}: définissez le code correct sur le mode de paiement {1}.,
|
||||
Row {0}: Qty is mandatory,Ligne {0} : Qté obligatoire,
|
||||
Row {0}: Quality Inspection rejected for item {1},Ligne {0}: le contrôle qualité a été rejeté pour l'élément {1}.,
|
||||
Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion LDM est obligatoire,
|
||||
Row {0}: UOM Conversion Factor is mandatory,Ligne {0} : Facteur de Conversion nomenclature est obligatoire,
|
||||
Row {0}: select the workstation against the operation {1},Ligne {0}: sélectionnez le poste de travail en fonction de l'opération {1},
|
||||
Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.,Ligne {0}: {1} Numéros de série requis pour l'article {2}. Vous en avez fourni {3}.,
|
||||
Row {0}: {1} must be greater than 0,Ligne {0}: {1} doit être supérieure à 0,
|
||||
@ -2587,8 +2587,8 @@ See past quotations,Voir les citations passées,
|
||||
Select,Sélectionner,
|
||||
Select Alternate Item,Sélectionnez un autre élément,
|
||||
Select Attribute Values,Sélectionner les valeurs d'attribut,
|
||||
Select BOM,Sélectionner LDM,
|
||||
Select BOM and Qty for Production,Sélectionner la LDM et la Qté pour la Production,
|
||||
Select BOM,Sélectionner une nomenclature,
|
||||
Select BOM and Qty for Production,Sélectionner la nomenclature et la Qté pour la Production,
|
||||
"Select BOM, Qty and For Warehouse","Sélectionner une nomenclature, une quantité et un entrepôt",
|
||||
Select Batch,Sélectionnez le Lot,
|
||||
Select Batch Numbers,Sélectionnez les Numéros de Lot,
|
||||
@ -2760,7 +2760,7 @@ Source and target warehouse cannot be same for row {0},L'entrepôt source et des
|
||||
Source and target warehouse must be different,Entrepôt source et destination doivent être différents,
|
||||
Source of Funds (Liabilities),Source des Fonds (Passif),
|
||||
Source warehouse is mandatory for row {0},Entrepôt source est obligatoire à la ligne {0},
|
||||
Specified BOM {0} does not exist for Item {1},La LDM {0} spécifiée n'existe pas pour l'Article {1},
|
||||
Specified BOM {0} does not exist for Item {1},La nomenclature {0} spécifiée n'existe pas pour l'Article {1},
|
||||
Split,Fractionner,
|
||||
Split Batch,Lot Fractionné,
|
||||
Split Issue,Diviser le ticket,
|
||||
@ -2888,11 +2888,11 @@ Supplies made to UIN holders,Fournitures faites aux titulaires de l'UIN,
|
||||
Supplies made to Unregistered Persons,Fournitures faites à des personnes non inscrites,
|
||||
Suppliies made to Composition Taxable Persons,Suppleies à des personnes assujetties à la composition,
|
||||
Supply Type,Type d'approvisionnement,
|
||||
Support,Soutien,
|
||||
Support Analytics,Analyse du Support,
|
||||
Support Settings,Paramètres du Support,
|
||||
Support Tickets,Billets de Support,
|
||||
Support queries from customers.,Demande de support des clients,
|
||||
Support,"Assistance/Support",
|
||||
Support Analytics,Analyse de l'assistance,
|
||||
Support Settings,Paramètres du module Assistance,
|
||||
Support Tickets,Ticket d'assistance,
|
||||
Support queries from customers.,Demande d'assistance des clients,
|
||||
Susceptible,Sensible,
|
||||
Sync has been temporarily disabled because maximum retries have been exceeded,La synchronisation a été temporairement désactivée car les tentatives maximales ont été dépassées,
|
||||
Syntax error in condition: {0},Erreur de syntaxe dans la condition: {0},
|
||||
@ -2965,7 +2965,7 @@ The name of the institute for which you are setting up this system.,Le nom de l'
|
||||
The name of your company for which you are setting up this system.,Le nom de l'entreprise pour laquelle vous configurez ce système.,
|
||||
The number of shares and the share numbers are inconsistent,Le nombre d'actions dans les transactions est incohérent avec le nombre total d'actions,
|
||||
The payment gateway account in plan {0} is different from the payment gateway account in this payment request,Le compte passerelle de paiement dans le plan {0} est différent du compte passerelle de paiement dans cette requête de paiement.,
|
||||
The selected BOMs are not for the same item,Les LDMs sélectionnées ne sont pas pour le même article,
|
||||
The selected BOMs are not for the same item,Les nomenclatures sélectionnées ne sont pas pour le même article,
|
||||
The selected item cannot have Batch,L’article sélectionné ne peut pas avoir de Lot,
|
||||
The seller and the buyer cannot be the same,Le vendeur et l'acheteur ne peuvent pas être les mêmes,
|
||||
The shareholder does not belong to this company,L'actionnaire n'appartient pas à cette société,
|
||||
@ -3150,7 +3150,7 @@ Transporter Name,Nom du transporteur,
|
||||
Travel,Déplacement,
|
||||
Travel Expenses,Frais de Déplacement,
|
||||
Tree Type,Type d'Arbre,
|
||||
Tree of Bill of Materials,Arbre des Listes de Matériaux,
|
||||
Tree of Bill of Materials,Arbre des Nomenclatures,
|
||||
Tree of Item Groups.,Arbre de Groupes d’Articles .,
|
||||
Tree of Procedures,Arbre de procédures,
|
||||
Tree of Quality Procedures.,Arbre de la qualité des procédures.,
|
||||
@ -3305,7 +3305,7 @@ Wire Transfer,Virement,
|
||||
WooCommerce Products,Produits WooCommerce,
|
||||
Work In Progress,Travaux en cours,
|
||||
Work Order,Ordre de travail,
|
||||
Work Order already created for all items with BOM,Ordre de travail déjà créé pour tous les articles avec une LDM,
|
||||
Work Order already created for all items with BOM,Ordre de travail déjà créé pour tous les articles avec une nomenclature,
|
||||
Work Order cannot be raised against a Item Template,Un ordre de travail ne peut pas être créé pour un modèle d'article,
|
||||
Work Order has been {0},L'ordre de travail a été {0},
|
||||
Work Order not created,Ordre de travail non créé,
|
||||
@ -3326,7 +3326,7 @@ You are not authorized to add or update entries before {0},Vous n'êtes pas auto
|
||||
You are not authorized to approve leaves on Block Dates,Vous n'êtes pas autorisé à approuver les congés sur les Dates Bloquées,
|
||||
You are not authorized to set Frozen value,Vous n'êtes pas autorisé à définir des valeurs gelées,
|
||||
You are not present all day(s) between compensatory leave request days,Vous n'êtes pas présent(e) tous les jours vos demandes de congé compensatoire,
|
||||
You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la LDM est mentionnée pour un article,
|
||||
You can not change rate if BOM mentioned agianst any item,Vous ne pouvez pas modifier le taux si la nomenclature est mentionnée pour un article,
|
||||
You can not enter current voucher in 'Against Journal Entry' column,Vous ne pouvez pas entrer le bon actuel dans la colonne 'Pour l'Écriture de Journal',
|
||||
You can only have Plans with the same billing cycle in a Subscription,Vous ne pouvez avoir que des plans ayant le même cycle de facturation dans le même abonnement,
|
||||
You can only redeem max {0} points in this order.,Vous pouvez uniquement échanger un maximum de {0} points dans cet commande.,
|
||||
@ -5502,7 +5502,7 @@ Blanket Order,Commande avec limites,
|
||||
Blanket Order Rate,Prix unitaire de commande avec limites,
|
||||
Returned Qty,Qté Retournée,
|
||||
Purchase Order Item Supplied,Article Fourni du Bon de Commande,
|
||||
BOM Detail No,N° de Détail LDM,
|
||||
BOM Detail No,N° de Détail de la nomenclature,
|
||||
Stock Uom,UDM du Stock,
|
||||
Raw Material Item Code,Code d’Article de Matière Première,
|
||||
Supplied Qty,Qté Fournie,
|
||||
@ -5600,7 +5600,6 @@ Call Log,Journal d'appel,
|
||||
Received By,Reçu par,
|
||||
Caller Information,Informations sur l'appelant,
|
||||
Contact Name,Nom du Contact,
|
||||
Lead ,Conduire,
|
||||
Lead Name,Nom du Prospect,
|
||||
Ringing,Sonnerie,
|
||||
Missed,Manqué,
|
||||
@ -7183,7 +7182,7 @@ Blanket Order Item,Article de commande avec limites,
|
||||
Ordered Quantity,Quantité Commandée,
|
||||
Item to be manufactured or repacked,Article à produire ou à réemballer,
|
||||
Quantity of item obtained after manufacturing / repacking from given quantities of raw materials,Quantité d'article obtenue après production / reconditionnement des quantités données de matières premières,
|
||||
Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la LDM,
|
||||
Set rate of sub-assembly item based on BOM,Définir le prix des articles de sous-assemblage en fonction de la nomenclature,
|
||||
Allow Alternative Item,Autoriser un article alternatif,
|
||||
Item UOM,UDM de l'Article,
|
||||
Conversion Rate,Taux de Conversion,
|
||||
@ -7214,33 +7213,33 @@ Website Specifications,Spécifications du Site Web,
|
||||
Show Items,Afficher les Articles,
|
||||
Show Operations,Afficher Opérations,
|
||||
Website Description,Description du Site Web,
|
||||
BOM Explosion Item,Article Eclaté LDM,
|
||||
BOM Explosion Item,Article Eclaté en nomenclature,
|
||||
Qty Consumed Per Unit,Qté Consommée Par Unité,
|
||||
Include Item In Manufacturing,Inclure l'article dans la fabrication,
|
||||
BOM Item,Article LDM,
|
||||
BOM Item,Article de la nomenclature,
|
||||
Item operation,Opération de l'article,
|
||||
Rate & Amount,Taux et Montant,
|
||||
Basic Rate (Company Currency),Taux de Base (Devise de la Société ),
|
||||
Scrap %,% de Rebut,
|
||||
Original Item,Article original,
|
||||
BOM Operation,Opération LDM,
|
||||
BOM Operation,Opération de la nomenclature (gamme),
|
||||
Operation Time ,Durée de l'opération,
|
||||
In minutes,En minutes,
|
||||
Batch Size,Taille du lot,
|
||||
Base Hour Rate(Company Currency),Taux Horaire de Base (Devise de la Société),
|
||||
Operating Cost(Company Currency),Coût d'Exploitation (Devise Société),
|
||||
BOM Scrap Item,Article Mis au Rebut LDM,
|
||||
BOM Scrap Item,Article Mis au Rebut dans la nomenclature,
|
||||
Basic Amount (Company Currency),Montant de Base (Devise de la Société),
|
||||
BOM Update Tool,Outil de mise à jour de LDM,
|
||||
"Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Remplacez une LDM particulière dans toutes les LDM où elles est utilisée. Cela remplacera le lien vers l'ancienne LDM, mettra à jour les coûts et régénérera le tableau ""Article Explosé de LDM"" selon la nouvelle LDM. Cela mettra également à jour les prix les plus récents dans toutes les LDMs.",
|
||||
Replace BOM,Remplacer la LDM,
|
||||
Current BOM,LDM Actuelle,
|
||||
The BOM which will be replaced,La LDM qui sera remplacée,
|
||||
The new BOM after replacement,La nouvelle LDM après remplacement,
|
||||
BOM Update Tool,Outil de mise à jour des Nomenclatures,
|
||||
"Replace a particular BOM in all other BOMs where it is used. It will replace the old BOM link, update cost and regenerate ""BOM Explosion Item"" table as per new BOM.\nIt also updates latest price in all the BOMs.","Remplacez une nomenclature particulière dans toutes les nomenclatures où elles est utilisée. Cela remplacera le lien vers l'ancienne nomenclature, mettra à jour les coûts et régénérera le tableau ""Article Explosé de nomenclature"" selon la nouvelle nomenclature. Cela mettra également à jour les prix les plus récents dans toutes les nomenclatures.",
|
||||
Replace BOM,Remplacer la nomenclature,
|
||||
Current BOM,nomenclature Actuelle,
|
||||
The BOM which will be replaced,La nomenclature qui sera remplacée,
|
||||
The new BOM after replacement,La nouvelle nomenclature après remplacement,
|
||||
Replace,Remplacer,
|
||||
Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les LDMs,
|
||||
BOM Website Item,Article de LDM du Site Internet,
|
||||
BOM Website Operation,Opération de LDM du Site Internet,
|
||||
Update latest price in all BOMs,Mettre à jour le prix le plus récent dans toutes les nomenclatures,
|
||||
BOM Website Item,Article de nomenclature du Site Internet,
|
||||
BOM Website Operation,Opération de nomenclature du Site Internet,
|
||||
Operation Time,Heure de l'Opération,
|
||||
PO-JOB.#####,PO-JOB. #####,
|
||||
Timing Detail,Détail du timing,
|
||||
@ -7272,7 +7271,7 @@ Default Scrap Warehouse,Entrepôt de rebut par défaut,
|
||||
Overproduction Percentage For Sales Order,Pourcentage de surproduction pour les commandes client,
|
||||
Overproduction Percentage For Work Order,Pourcentage de surproduction pour les ordres de travail,
|
||||
Other Settings,Autres Paramètres,
|
||||
Update BOM Cost Automatically,Mettre à jour automatiquement le coût de la LDM,
|
||||
Update BOM Cost Automatically,Mettre à jour automatiquement le coût de la nomenclature,
|
||||
Material Request Plan Item,Article du plan de demande de matériel,
|
||||
Material Request Type,Type de Demande de Matériel,
|
||||
Material Issue,Sortie de Matériel,
|
||||
@ -7312,7 +7311,7 @@ MFG-WO-.YYYY.-,MFG-WO-.YYYY.-,
|
||||
Item To Manufacture,Article à produire,
|
||||
Material Transferred for Manufacturing,Matériel Transféré pour la Production,
|
||||
Manufactured Qty,Qté Produite,
|
||||
Use Multi-Level BOM,Utiliser LDM à Plusieurs Niveaux,
|
||||
Use Multi-Level BOM,Utiliser les nomenclatures à plusieurs niveaux,
|
||||
Plan material for sub-assemblies,Plan de matériaux pour les sous-ensembles,
|
||||
Skip Material Transfer to WIP Warehouse,Ignorer le transfert de matériel vers l'entrepôt WIP,
|
||||
Check if material transfer entry is not required,Vérifiez si une un transfert de matériel n'est pas requis,
|
||||
@ -7685,7 +7684,7 @@ Collected Amount,Montant collecté,
|
||||
Expected Amount,Montant prévu,
|
||||
POS Closing Voucher Invoices,Factures du bon de clôture du PDV,
|
||||
Quantity of Items,Quantité d'articles,
|
||||
"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé. Remarque: LDM = Liste\nDes Matériaux",
|
||||
"Aggregate group of **Items** into another **Item**. This is useful if you are bundling a certain **Items** into a package and you maintain stock of the packed **Items** and not the aggregate **Item**. \n\nThe package **Item** will have ""Is Stock Item"" as ""No"" and ""Is Sales Item"" as ""Yes"".\n\nFor Example: If you are selling Laptops and Backpacks separately and have a special price if the customer buys both, then the Laptop + Backpack will be a new Product Bundle Item.\n\nNote: BOM = Bill of Materials","Regroupement d' **Articles** dans un autre **Article**. Ceci est utile si vous regroupez certains **Articles** dans un lot et que vous maintenez l'inventaire des **Articles** du lot et non de l'**Article** composé. L'**Article** composé aura ""Article En Stock"" à ""Non"" et ""Article À Vendre"" à ""Oui"". Exemple : Si vous vendez des Ordinateurs Portables et Sacs à Dos séparément et qu'il y a un prix spécial si le client achète les deux, alors l'Ordinateur Portable + le Sac à Dos sera un nouveau Produit Groupé.",
|
||||
Parent Item,Article Parent,
|
||||
List items that form the package.,Liste des articles qui composent le paquet.,
|
||||
SAL-QTN-.YYYY.-,SAL-QTN-. AAAA.-,
|
||||
@ -8089,7 +8088,7 @@ Customer Items,Articles du clients,
|
||||
Inspection Criteria,Critères d'Inspection,
|
||||
Inspection Required before Purchase,Inspection Requise avant Achat,
|
||||
Inspection Required before Delivery,Inspection Requise avant Livraison,
|
||||
Default BOM,LDM par Défaut,
|
||||
Default BOM,Nomenclature par Défaut,
|
||||
Supply Raw Materials for Purchase,Fournir les Matières Premières pour l'Achat,
|
||||
If subcontracted to a vendor,Si sous-traité à un fournisseur,
|
||||
Customer Code,Code Client,
|
||||
@ -8295,7 +8294,7 @@ Delivery Note No,Bon de Livraison N°,
|
||||
Sales Invoice No,N° de la Facture de Vente,
|
||||
Purchase Receipt No,N° du Reçu d'Achat,
|
||||
Inspection Required,Inspection obligatoire,
|
||||
From BOM,De LDM,
|
||||
From BOM,Depuis la nomenclature,
|
||||
For Quantity,Pour la Quantité,
|
||||
As per Stock UOM,Selon UDM du Stock,
|
||||
Including items for sub assemblies,Incluant les articles pour des sous-ensembles,
|
||||
@ -8316,7 +8315,7 @@ Basic Rate (as per Stock UOM),Taux de base (comme l’UDM du Stock),
|
||||
Basic Amount,Montant de Base,
|
||||
Additional Cost,Frais Supplémentaire,
|
||||
Serial No / Batch,N° de Série / Lot,
|
||||
BOM No. for a Finished Good Item,N° d’Article Produit Fini LDM,
|
||||
BOM No. for a Finished Good Item,N° de nomenclature pour un d’Article (Produit Fini),
|
||||
Material Request used to make this Stock Entry,Demande de Matériel utilisée pour réaliser cette Écriture de Stock,
|
||||
Subcontracted Item,Article sous-traité,
|
||||
Against Stock Entry,Contre entrée de stock,
|
||||
@ -8456,9 +8455,9 @@ Bank Remittance,Virement bancaire,
|
||||
Batch Item Expiry Status,Statut d'Expiration d'Article du Lot,
|
||||
Batch-Wise Balance History,Historique de Balance des Lots,
|
||||
BOM Explorer,Explorateur de nomenclature,
|
||||
BOM Search,Recherche LDM,
|
||||
BOM Stock Calculated,Stock calculé par liste de matériaux (LDM),
|
||||
BOM Variance Report,Rapport de variance par liste de matériaux (LDM),
|
||||
BOM Search,Recherche nomenclature,
|
||||
BOM Stock Calculated,Stock calculé par nomenclature,
|
||||
BOM Variance Report,Rapport de variance par nomenclature,
|
||||
Campaign Efficiency,Efficacité des Campagnes,
|
||||
Cash Flow,Flux de Trésorerie,
|
||||
Completed Work Orders,Ordres de travail terminés,
|
||||
@ -9873,3 +9872,7 @@ Convert Item Description to Clean HTML in Transactions,Convertir les description
|
||||
Have Default Naming Series for Batch ID?,Nom de série par défaut pour les Lots ou Séries
|
||||
"The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units","Le pourcentage de quantité que vous pourrez réceptionner en plus de la quantité commandée. Par exemple, vous avez commandé 100 unités, votre pourcentage de dépassement est de 10%, vous pourrez réceptionner 110 unités"
|
||||
Unit Of Measure (UOM),Unité de mesure (UDM),
|
||||
Allowed Items,Articles autorisés
|
||||
Party Specific Item,Restriction d'article disponible
|
||||
Restrict Items Based On,Type de critére de restriction
|
||||
Based On Value,critére de restriction
|
||||
|
Can't render this file because it is too large.
|
60
erpnext/utilities/naming.py
Normal file
60
erpnext/utilities/naming.py
Normal file
@ -0,0 +1,60 @@
|
||||
import frappe
|
||||
from frappe.model.naming import get_default_naming_series
|
||||
|
||||
|
||||
class NamingSeriesNotSetError(frappe.ValidationError):
|
||||
pass
|
||||
|
||||
|
||||
def set_by_naming_series(
|
||||
doctype, fieldname, naming_series, hide_name_field=True, make_mandatory=1
|
||||
):
|
||||
"""Change a doctype's naming to user naming series"""
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
|
||||
if naming_series:
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "hidden", 0, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "reqd", make_mandatory, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
|
||||
# set values for mandatory
|
||||
try:
|
||||
frappe.db.sql(
|
||||
"""update `tab{doctype}` set naming_series={s} where
|
||||
ifnull(naming_series, '')=''""".format(
|
||||
doctype=doctype, s="%s"
|
||||
),
|
||||
get_default_naming_series(doctype),
|
||||
)
|
||||
except NamingSeriesNotSetError:
|
||||
pass
|
||||
|
||||
if hide_name_field:
|
||||
make_property_setter(doctype, fieldname, "reqd", 0, "Check", validate_fields_for_doctype=False)
|
||||
make_property_setter(
|
||||
doctype, fieldname, "hidden", 1, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
else:
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "reqd", 0, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(
|
||||
doctype, "naming_series", "hidden", 1, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
|
||||
if hide_name_field:
|
||||
make_property_setter(
|
||||
doctype, fieldname, "hidden", 0, "Check", validate_fields_for_doctype=False
|
||||
)
|
||||
make_property_setter(doctype, fieldname, "reqd", 1, "Check", validate_fields_for_doctype=False)
|
||||
|
||||
# set values for mandatory
|
||||
frappe.db.sql(
|
||||
"""update `tab{doctype}` set `{fieldname}`=`name` where
|
||||
ifnull({fieldname}, '')=''""".format(
|
||||
doctype=doctype, fieldname=fieldname
|
||||
)
|
||||
)
|
Loading…
x
Reference in New Issue
Block a user