Merge branch 'develop' into fix-salary-slip-bg-job
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 69749c9..f0f83b0 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -11,7 +11,7 @@
cd ~ || exit
-sudo apt-get install redis-server libcups2-dev
+sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
diff --git a/cypress.json b/cypress.json
deleted file mode 100644
index 02b10d8..0000000
--- a/cypress.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "baseUrl": "http://test_site:8000/",
- "projectId": "da59y9",
- "adminPassword": "admin",
- "defaultCommandTimeout": 20000,
- "pageLoadTimeout": 15000,
- "retries": {
- "runMode": 2,
- "openMode": 2
- }
-}
diff --git a/cypress/fixtures/example.json b/cypress/fixtures/example.json
deleted file mode 100644
index da18d93..0000000
--- a/cypress/fixtures/example.json
+++ /dev/null
@@ -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"
-}
\ No newline at end of file
diff --git a/cypress/integration/test_bulk_transaction_processing.js b/cypress/integration/test_bulk_transaction_processing.js
deleted file mode 100644
index 428ec51..0000000
--- a/cypress/integration/test_bulk_transaction_processing.js
+++ /dev/null
@@ -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");
- });
-});
diff --git a/cypress/integration/test_customer.js b/cypress/integration/test_customer.js
deleted file mode 100644
index 3d6ed5d..0000000
--- a/cypress/integration/test_customer.js
+++ /dev/null
@@ -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');
- });
-});
diff --git a/cypress/integration/test_item.js b/cypress/integration/test_item.js
deleted file mode 100644
index fcb7533..0000000
--- a/cypress/integration/test_item.js
+++ /dev/null
@@ -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");
- });
-});
diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js
deleted file mode 100644
index 464cce4..0000000
--- a/cypress/integration/test_organizational_chart_desktop.js
+++ /dev/null
@@ -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]}`);
- });
- });
-});
diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js
deleted file mode 100644
index 971ac6d..0000000
--- a/cypress/integration/test_organizational_chart_mobile.js
+++ /dev/null
@@ -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]}`);
- });
- });
-});
diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js
deleted file mode 100644
index 07d9804..0000000
--- a/cypress/plugins/index.js
+++ /dev/null
@@ -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
-};
diff --git a/cypress/support/commands.js b/cypress/support/commands.js
deleted file mode 100644
index 7ddc80a..0000000
--- a/cypress/support/commands.js
+++ /dev/null
@@ -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)}`);
-});
diff --git a/cypress/support/index.js b/cypress/support/index.js
deleted file mode 100644
index 72070cc..0000000
--- a/cypress/support/index.js
+++ /dev/null
@@ -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'
-});
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
deleted file mode 100644
index d90ebf6..0000000
--- a/cypress/tsconfig.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "compilerOptions": {
- "allowJs": true,
- "baseUrl": "../node_modules",
- "types": [
- "cypress"
- ]
- },
- "include": [
- "**/*.*"
- ]
-}
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 056084b..bebfa6c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1866,7 +1866,7 @@
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 @@
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]
@@ -2661,7 +2661,8 @@
parent.update_reserved_qty_for_subcontract()
parent.create_raw_materials_supplied("supplied_items")
parent.save()
- else:
+ else: # Sales Order
+ parent.validate_warehouse()
parent.update_reserved_qty()
parent.update_project()
parent.update_prevdoc_status("submit")
diff --git a/erpnext/hr/doctype/attendance/test_attendance.py b/erpnext/hr/doctype/attendance/test_attendance.py
index 762d0f7..c85ec65 100644
--- a/erpnext/hr/doctype/attendance/test_attendance.py
+++ b/erpnext/hr/doctype/attendance/test_attendance.py
@@ -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 @@
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]
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 1829bc4..f09d7ff 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -230,7 +230,7 @@
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 @@
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 @@
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,
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index a9659c3..8594ebb 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -231,7 +231,6 @@
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
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
deleted file mode 100644
index 59b17ee..0000000
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ /dev/null
@@ -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})
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index edc4b06..de93c82 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -423,7 +423,7 @@
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 @@
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,
diff --git a/erpnext/public/js/utils/barcode_scanner.js b/erpnext/public/js/utils/barcode_scanner.js
index 943db07..a6bff2c 100644
--- a/erpnext/public/js/utils/barcode_scanner.js
+++ b/erpnext/public/js/utils/barcode_scanner.js
@@ -9,6 +9,7 @@
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 @@
// 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";
}
@@ -67,9 +69,9 @@
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) {
@@ -90,10 +92,11 @@
}
frappe.run_serially([
- () => this.set_selector_trigger_flag(row, data),
+ () => 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),
@@ -106,7 +109,7 @@
// 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;
@@ -154,6 +157,12 @@
}
}
+ 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)) {
await frappe.model.set_value(row.doctype, row.name, this.batch_no_field, batch_no);
@@ -184,7 +193,7 @@
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
@@ -193,10 +202,12 @@
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)
}
diff --git a/erpnext/stock/doctype/item_barcode/item_barcode.json b/erpnext/stock/doctype/item_barcode/item_barcode.json
index eef70c9..56832f3 100644
--- a/erpnext/stock/doctype/item_barcode/item_barcode.json
+++ b/erpnext/stock/doctype/item_barcode/item_barcode.json
@@ -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",
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index 2120304..6d8fdaa 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -558,7 +558,7 @@
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: