Merge branch 'version-13-hotfix' into si_total_billing_hours_v13

This commit is contained in:
Raffael Meyer
2021-08-15 18:45:33 +02:00
committed by GitHub
160 changed files with 6224 additions and 2444 deletions

View File

@@ -147,10 +147,15 @@
"Chart": true,
"Cypress": true,
"cy": true,
"describe": true,
"expect": true,
"it": true,
"context": true,
"before": true,
"beforeEach": true,
"onScan": true
"onScan": true,
"html2canvas": true,
"extend_cscript": true,
"localforage": true
}
}

View File

@@ -42,5 +42,5 @@ sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
bench start &
bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes

View File

@@ -1,6 +1,12 @@
name: Patch
on: [pull_request, workflow_dispatch]
on:
pull_request:
paths-ignore:
- '**.js'
- '**.md'
workflow_dispatch:
jobs:
test:

View File

@@ -1,6 +1,16 @@
name: Server
on: [pull_request, workflow_dispatch]
on:
pull_request:
paths-ignore:
- '**.js'
- '**.md'
workflow_dispatch:
push:
branches: [ develop ]
paths-ignore:
- '**.js'
- '**.md'
jobs:
test:

110
.github/workflows/ui-tests.yml vendored Normal file
View File

@@ -0,0 +1,110 @@
name: UI
on:
pull_request:
paths-ignore:
- '**.md'
workflow_dispatch:
jobs:
test:
runs-on: ubuntu-18.04
strategy:
fail-fast: false
name: UI Tests (Cypress)
services:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
steps:
- name: Clone
uses: actions/checkout@v2
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: 3.7
- uses: actions/setup-node@v2
with:
node-version: 14
check-latest: true
- name: Add to Hosts
run: |
echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
- name: Cache pip
uses: actions/cache@v2
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
restore-keys: |
${{ runner.os }}-pip-
${{ runner.os }}-
- name: Cache node modules
uses: actions/cache@v2
env:
cache-name: cache-node-modules
with:
path: ~/.npm
key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-build-${{ env.cache-name }}-
${{ runner.os }}-build-
${{ runner.os }}-
- name: Get yarn cache directory path
id: yarn-cache-dir-path
run: echo "::set-output name=dir::$(yarn cache dir)"
- uses: actions/cache@v2
id: yarn-cache
with:
path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
restore-keys: |
${{ runner.os }}-yarn-
- name: Cache cypress binary
uses: actions/cache@v2
with:
path: ~/.cache
key: ${{ runner.os }}-cypress-
restore-keys: |
${{ runner.os }}-cypress-
${{ runner.os }}-
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
DB: mariadb
TYPE: ui
- name: Site Setup
run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
- name: cypress pre-requisites
run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 --no-lockfile
- name: Build Assets
run: cd ~/frappe-bench/ && bench build
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
env:
CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd
- name: Show bench console if tests failed
if: ${{ failure() }}
run: cat ~/frappe-bench/bench_run_logs.txt

11
cypress.json Normal file
View File

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

View File

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

View File

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

View File

@@ -0,0 +1,111 @@
context('Organizational Chart', () => {
before(() => {
cy.login();
cy.visit('/app/website');
cy.awesomebar('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{enter}', { force: true })
.blur({ force: true });
});
});
});
it('renders root nodes and loads children for the first expandable node', () => {
// check rendered root nodes and the node name, title, connections
cy.get('.hierarchy').find('.root-level ul.node-children').children()
.should('have.length', 2)
.first()
.as('first-child');
cy.get('@first-child').get('.node-name').contains('Test Employee 1');
cy.get('@first-child').get('.node-info').find('.node-title').contains('CEO');
cy.get('@first-child').get('.node-info').find('.node-connections').contains('· 2 Connections');
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// children of 1st root visible
cy.get(`div[data-parent="${employee_records.message[0]}"]`).as('child-node');
cy.get('@child-node')
.should('have.length', 1)
.should('be.visible');
cy.get('@child-node').get('.node-name').contains('Test Employee 3');
// connectors between first root node and immediate child
cy.get(`path[data-parent="${employee_records.message[0]}"]`)
.should('be.visible')
.invoke('attr', 'data-child')
.should('equal', employee_records.message[2]);
});
});
it('hides active nodes children and connectors on expanding sibling node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// click sibling
cy.get(`#${employee_records.message[1]}`)
.click()
.should('have.class', 'active');
// child nodes and connectors hidden
cy.get(`[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
cy.get(`path[data-parent="${employee_records.message[0]}"]`).should('not.be.visible');
});
});
it('collapses previous level nodes and refreshes connectors on expanding child node', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
// click child node
cy.get(`#${employee_records.message[3]}`)
.click()
.should('have.class', 'active');
// previous level nodes: parent should be on active-path; other nodes should be collapsed
cy.get(`#${employee_records.message[0]}`).should('have.class', 'collapsed');
cy.get(`#${employee_records.message[1]}`).should('have.class', 'active-path');
// previous level connectors refreshed
cy.get(`path[data-parent="${employee_records.message[1]}"]`)
.should('have.class', 'collapsed-connector');
// child node's children and connectors rendered
cy.get(`[data-parent="${employee_records.message[3]}"]`).should('be.visible');
cy.get(`path[data-parent="${employee_records.message[3]}"]`).should('be.visible');
});
});
it('expands previous level nodes', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`)
.click()
.should('have.class', 'active');
cy.get(`[data-parent="${employee_records.message[0]}"]`)
.should('be.visible');
cy.get('ul.hierarchy').children().should('have.length', 2);
cy.get(`#connectors`).children().should('have.length', 1);
});
});
it('edit node navigates to employee master', () => {
cy.call('erpnext.tests.ui_test_helpers.get_employee_records').then(employee_records => {
cy.get(`#${employee_records.message[0]}`).find('.btn-edit-node')
.click();
cy.url().should('include', `/employee/${employee_records.message[0]}`);
});
});
});

View File

@@ -0,0 +1,190 @@
context('Organizational Chart Mobile', () => {
before(() => {
cy.login();
cy.viewport(375, 667);
cy.visit('/app/website');
cy.awesomebar('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{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]}`);
});
});
});

17
cypress/plugins/index.js Normal file
View File

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

View File

@@ -0,0 +1,31 @@
// ***********************************************
// 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)}`);
});

26
cypress/support/index.js Normal file
View File

@@ -0,0 +1,26 @@
// ***********************************************************
// 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'
});

12
cypress/tsconfig.json Normal file
View File

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

View File

@@ -230,7 +230,7 @@ class Account(NestedSet):
if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group."))
elif self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
throw(_("Cannot convert to Group because Account Type is selected."))
else:
self.is_group = 1
self.save()

View File

@@ -19,6 +19,7 @@
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
"post_change_gl_entries",
"enable_discount_accounting",
"tax_settings_section",
"determine_address_tax_category_from",
"column_break_19",
@@ -261,6 +262,13 @@
"fieldname": "post_change_gl_entries",
"fieldtype": "Check",
"label": "Create Ledger Entries for Change Amount"
},
{
"default": "0",
"description": "If enabled, additional ledger entries will be made for discounts in a separate Discount Account",
"fieldname": "enable_discount_accounting",
"fieldtype": "Check",
"label": "Enable Discount Accounting"
}
],
"icon": "icon-cog",
@@ -268,7 +276,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-06-17 20:26:03.721202",
"modified": "2021-07-12 18:54:29.084958",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@@ -21,6 +21,7 @@ class AccountsSettings(Document):
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@@ -33,3 +34,22 @@ class AccountsSettings(Document):
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
if enable_discount_accounting:
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
else:
make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
for doctype in ["Sales Invoice", "Purchase Invoice"]:
make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
if enable_discount_accounting:
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
else:
make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)

View File

@@ -249,7 +249,7 @@ class TestBudget(unittest.TestCase):
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project":
budget_against = "_Test Project"
budget_against = frappe.db.get_value("Project", {"project_name": "_Test Project"})
else:
budget_against = budget_against_CC or "_Test Cost Center - _TC"
@@ -275,7 +275,7 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
elif budget_against_field == "project":
make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project="_Test Project", posting_date=nowdate())
"_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
def make_budget(**args):
args = frappe._dict(args)

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-06 16:18:25.410476",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"campaign"
],
"fields": [
{
"fieldname": "campaign",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Campaign",
"options": "Campaign"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 10:43:49.717633",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Campaign Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -57,7 +57,7 @@ def test_create_test_data():
})
item_price.insert()
# create test item pricing rule
if not frappe.db.exists("Pricing Rule","_Test Pricing Rule for _Test Item"):
if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}):
item_pricing_rule = frappe.get_doc({
"doctype": "Pricing Rule",
"title": "_Test Pricing Rule for _Test Item",
@@ -86,14 +86,15 @@ def test_create_test_data():
sales_partner.insert()
# create test item coupon code
if not frappe.db.exists("Coupon Code", "SAVE30"):
pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name'])
coupon_code = frappe.get_doc({
"doctype": "Coupon Code",
"coupon_name":"SAVE30",
"coupon_code":"SAVE30",
"pricing_rule": "_Test Pricing Rule for _Test Item",
"valid_from": "2014-01-01",
"maximum_use":1,
"used":0
"doctype": "Coupon Code",
"coupon_name":"SAVE30",
"coupon_code":"SAVE30",
"pricing_rule": pricing_rule,
"valid_from": "2014-01-01",
"maximum_use":1,
"used":0
})
coupon_code.insert()
@@ -102,7 +103,7 @@ class TestCouponCode(unittest.TestCase):
test_create_test_data()
def tearDown(self):
frappe.set_user("Administrator")
frappe.set_user("Administrator")
def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-06 16:12:42.558878",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer_group"
],
"fields": [
{
"fieldname": "customer_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer Group",
"options": "Customer Group"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 10:39:21.563506",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Customer Group Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-05 14:04:54.266353",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"customer"
],
"fields": [
{
"fieldname": "customer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Customer ",
"options": "Customer"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-06 10:02:32.967841",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Customer Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -58,8 +58,8 @@ class GLEntry(Document):
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
.format(self.voucher_type, self.voucher_no, self.account))
@@ -73,15 +73,19 @@ class GLEntry(Document):
.format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self):
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
"""Validate that profit and loss type account GL entries have a cost center."""
frappe.throw(msg, title=_("Missing Cost Center"))
if self.cost_center or self.voucher_type == 'Period Closing Voucher':
return
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
self.voucher_type, self.voucher_no, self.account)
msg += " "
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")

View File

@@ -2,12 +2,13 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:title",
"autoname": "naming_series:",
"creation": "2014-02-21 15:02:51",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"applicability_section",
"naming_series",
"title",
"disable",
"apply_on",
@@ -95,8 +96,7 @@
"fieldtype": "Data",
"label": "Title",
"no_copy": 1,
"reqd": 1,
"unique": 1
"reqd": 1
},
{
"default": "0",
@@ -558,7 +558,8 @@
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
"label": "Condition",
"options": "PythonExpression"
},
{
"fieldname": "column_break_42",
@@ -570,12 +571,19 @@
"fieldname": "is_recursive",
"fieldtype": "Check",
"label": "Is Recursive"
},
{
"default": "PRLE-.####",
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "PRLE-.####"
}
],
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2021-03-06 22:01:24.840422",
"modified": "2021-08-06 15:10:04.219321",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
@@ -633,5 +641,6 @@
],
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC"
}
"sort_order": "DESC",
"title_field": "title"
}

View File

@@ -25,22 +25,31 @@ product_discount_fields = ['free_item', 'free_qty', 'free_item_uom',
class PromotionalScheme(Document):
def validate(self):
if not self.selling and not self.buying:
frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory"))
if not (self.price_discount_slabs
or self.product_discount_slabs):
frappe.throw(_("Price or product discount slabs are required"))
def on_update(self):
data = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
filters = {'promotional_scheme': self.name}) or {}
pricing_rules = frappe.get_all(
'Pricing Rule',
fields = ["promotional_scheme_id", "name", "creation"],
filters = {
'promotional_scheme': self.name,
'applicable_for': self.applicable_for
},
order_by = 'creation asc',
) or {}
self.update_pricing_rules(pricing_rules)
self.update_pricing_rules(data)
def update_pricing_rules(self, data):
def update_pricing_rules(self, pricing_rules):
rules = {}
count = 0
for d in data:
rules[d.get('promotional_scheme_id')] = d.get('name')
names = []
for rule in pricing_rules:
names.append(rule.name)
rules[rule.get('promotional_scheme_id')] = names
docs = get_pricing_rules(self, rules)
@@ -57,9 +66,9 @@ class PromotionalScheme(Document):
frappe.msgprint(_("New {0} pricing rules are created").format(count))
def on_trash(self):
for d in frappe.get_all('Pricing Rule',
for rule in frappe.get_all('Pricing Rule',
{'promotional_scheme': self.name}):
frappe.delete_doc('Pricing Rule', d.name)
frappe.delete_doc('Pricing Rule', rule.name)
def get_pricing_rules(doc, rules = {}):
new_doc = []
@@ -73,42 +82,80 @@ def get_pricing_rules(doc, rules = {}):
def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
new_doc = []
args = get_args_for_pricing_rule(doc)
for d in doc.get(child_doc):
applicable_for = frappe.scrub(doc.get('applicable_for'))
for idx, d in enumerate(doc.get(child_doc)):
if d.name in rules:
pr = frappe.get_doc('Pricing Rule', rules.get(d.name))
for applicable_for_value in args.get(applicable_for):
temp_args = args.copy()
docname = frappe.get_all(
'Pricing Rule',
fields = ["promotional_scheme_id", "name", applicable_for],
filters = {
'promotional_scheme_id': d.name,
applicable_for: applicable_for_value
}
)
if docname:
pr = frappe.get_doc('Pricing Rule', docname[0].get('name'))
temp_args[applicable_for] = applicable_for_value
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
else:
pr = frappe.new_doc("Pricing Rule")
pr.title = doc.name
temp_args[applicable_for] = applicable_for_value
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
new_doc.append(pr)
else:
pr = frappe.new_doc("Pricing Rule")
pr.title = make_autoname("{0}/.####".format(doc.name))
pr.update(args)
for field in (other_fields + discount_fields):
pr.set(field, d.get(field))
pr.promotional_scheme_id = d.name
pr.promotional_scheme = doc.name
pr.disable = d.disable if d.disable else doc.disable
pr.price_or_product_discount = ('Price'
if child_doc == 'price_discount_slabs' else 'Product')
for field in ['items', 'item_groups', 'brands']:
if doc.get(field):
pr.set(field, [])
apply_on = frappe.scrub(doc.get('apply_on'))
for d in doc.get(field):
pr.append(field, {
apply_on: d.get(apply_on),
'uom': d.uom
})
new_doc.append(pr)
applicable_for_values = args.get(applicable_for) or []
for applicable_for_value in applicable_for_values:
pr = frappe.new_doc("Pricing Rule")
pr.title = doc.name
temp_args = args.copy()
temp_args[applicable_for] = applicable_for_value
pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d)
new_doc.append(pr)
return new_doc
def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields):
pr.update(args)
for field in (other_fields + discount_fields):
pr.set(field, child_doc_fields.get(field))
pr.promotional_scheme_id = child_doc_fields.name
pr.promotional_scheme = doc.name
pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable
pr.price_or_product_discount = ('Price'
if child_doc == 'price_discount_slabs' else 'Product')
for field in ['items', 'item_groups', 'brands']:
if doc.get(field):
pr.set(field, [])
apply_on = frappe.scrub(doc.get('apply_on'))
for d in doc.get(field):
pr.append(field, {
apply_on: d.get(apply_on),
'uom': d.uom
})
return pr
def get_args_for_pricing_rule(doc):
args = { 'promotional_scheme': doc.name }
applicable_for = frappe.scrub(doc.get('applicable_for'))
for d in pricing_rule_fields:
args[d] = doc.get(d)
if d == applicable_for:
items = []
for applicable_for_values in doc.get(applicable_for):
items.append(applicable_for_values.get(applicable_for))
args[d] = items
else:
args[d] = doc.get(d)
return args

View File

@@ -7,4 +7,54 @@ import frappe
import unittest
class TestPromotionalScheme(unittest.TestCase):
pass
def test_promotional_scheme(self):
ps = make_promotional_scheme()
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"],
filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules),1)
price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
self.assertTrue(price_doc_details.customer, '_Test Customer')
self.assertTrue(price_doc_details.min_qty, 4)
self.assertTrue(price_doc_details.discount_percentage, 20)
ps.price_discount_slabs[0].min_qty = 6
ps.append('customer', {
'customer': "_Test Customer 2"})
ps.save()
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
filters = {'promotional_scheme': ps.name})
self.assertTrue(len(price_rules), 2)
price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
self.assertTrue(price_doc_details.customer, '_Test Customer 2')
self.assertTrue(price_doc_details.min_qty, 6)
self.assertTrue(price_doc_details.discount_percentage, 20)
price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1)
self.assertTrue(price_doc_details.customer, '_Test Customer')
self.assertTrue(price_doc_details.min_qty, 6)
frappe.delete_doc('Promotional Scheme', ps.name)
price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"],
filters = {'promotional_scheme': ps.name})
self.assertEqual(price_rules, [])
def make_promotional_scheme():
ps = frappe.new_doc('Promotional Scheme')
ps.name = '_Test Scheme'
ps.append('items',{
'item_code': '_Test Item'
})
ps.selling = 1
ps.append('price_discount_slabs',{
'min_qty': 4,
'discount_percentage': 20,
'rule_description': 'Test'
})
ps.applicable_for = 'Customer'
ps.append('customer',{
'customer': "_Test Customer"
})
ps.save()
return ps

View File

@@ -134,7 +134,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
},
get_query_filters: {
docstatus: 1,
status: ["not in", ["Closed", "Completed"]],
status: ["not in", ["Closed", "Completed", "Return Issued"]],
company: me.frm.doc.company,
is_return: 0
}
@@ -275,7 +275,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
// Do not update if inter company reference is there as the details will already be updated
if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
return;
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{
posting_date: this.frm.doc.posting_date,
@@ -283,7 +283,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
party: this.frm.doc.supplier,
party_type: "Supplier",
account: this.frm.doc.credit_to,
price_list: this.frm.doc.buying_price_list
price_list: this.frm.doc.buying_price_list,
fetch_payment_terms_template: cint(!this.frm.doc.ignore_default_payment_terms_template)
}, function() {
me.apply_pricing_rule();
me.frm.doc.apply_tds = me.frm.supplier_tds ? 1 : 0;
@@ -365,7 +366,7 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({
items_add: function(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row,
["expense_account", "cost_center", "project"]);
["expense_account", "discount_account", "cost_center", "project"]);
},
on_submit: function() {
@@ -499,6 +500,16 @@ frappe.ui.form.on("Purchase Invoice", {
'Payment Entry': 'Payment'
}
frm.set_query("additional_discount_account", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
report_type: "Profit and Loss",
}
};
});
frm.fields_dict['items'].grid.get_field('deferred_expense_account').get_query = function(doc) {
return {
filters: {
@@ -508,6 +519,16 @@ frappe.ui.form.on("Purchase Invoice", {
}
}
}
frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) {
return {
filters: {
'report_type': 'Profit and Loss',
'company': doc.company,
"is_group": 0
}
}
}
},
refresh: function(frm) {

View File

@@ -22,7 +22,7 @@ from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accoun
from frappe.model.mapper import get_mapped_doc
from six import iteritems
from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
unlink_inter_company_doc
unlink_inter_company_doc, check_if_return_invoice_linked_with_payment_entry
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import get_item_account_wise_additional_cost
@@ -446,6 +446,7 @@ class PurchaseInvoice(BuyingController):
self.make_supplier_gl_entry(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_discount_gl_entries(gl_entries)
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries)
@@ -608,7 +609,7 @@ class PurchaseInvoice(BuyingController):
if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account)
if not item.is_fixed_asset:
amount = flt(item.base_net_amount, item.precision("base_net_amount"))
dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
else:
amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount"))
@@ -822,8 +823,10 @@ class PurchaseInvoice(BuyingController):
def make_tax_gl_entries(self, gl_entries):
# tax table gl entries
valuation_tax = {}
for tax in self.get("taxes"):
if tax.category in ("Total", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting)
if tax.category in ("Total", "Valuation and Total") and flt(base_amount):
account_currency = get_account_currency(tax.account_head)
dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
@@ -832,21 +835,21 @@ class PurchaseInvoice(BuyingController):
self.get_gl_dict({
"account": tax.account_head,
"against": self.supplier,
dr_or_cr: tax.base_tax_amount_after_discount_amount,
dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
if account_currency==self.company_currency \
else tax.tax_amount_after_discount_amount,
dr_or_cr: base_amount,
dr_or_cr + "_in_account_currency": base_amount
if account_currency==self.company_currency
else amount,
"cost_center": tax.cost_center
}, account_currency, item=tax)
)
# accumulate valuation tax
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \
if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(base_amount) \
and not self.is_internal_transfer():
if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
valuation_tax.setdefault(tax.name, 0)
valuation_tax[tax.name] += \
(tax.add_deduct_tax == "Add" and 1 or -1) * flt(tax.base_tax_amount_after_discount_amount)
(tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount)
if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax:
# credit valuation tax amount in "Expenses Included In Valuation"
@@ -887,6 +890,13 @@ class PurchaseInvoice(BuyingController):
"remarks": self.remarks or "Accounting Entry for Stock"
}, item=tax))
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
return self._enable_discount_accounting
def make_internal_transfer_gl_entries(self, gl_entries):
if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
account_currency = get_account_currency(self.unrealized_profit_loss_account)
@@ -982,6 +992,8 @@ class PurchaseInvoice(BuyingController):
}, item=self))
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
super(PurchaseInvoice, self).on_cancel()
self.check_on_hold_or_closed_status()

View File

@@ -230,6 +230,50 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
discount_account = create_account(account_name="Discount Account",
parent_account="Indirect Expenses - _TC", company="_Test Company")
pi = make_purchase_invoice(discount_account=discount_account, rate=45)
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
["Creditors - _TC", 0.0, 225.0, nowdate()],
["Discount Account - _TC", 0.0, 25.0, nowdate()]
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
enable_discount_accounting(enable=0)
def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self):
enable_discount_accounting()
additional_discount_account = create_account(account_name="Discount Account",
parent_account="Indirect Expenses - _TC", company="_Test Company")
pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC")
pi.apply_discount_on = "Grand Total"
pi.additional_discount_account = additional_discount_account
pi.additional_discount_percentage = 10
pi.disable_rounded_total = 1
pi.append("taxes", {
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"description": "Test",
"rate": 10
})
pi.submit()
expected_gle = [
["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()],
["_Test Account VAT - _TC", 25.0, 0.0, nowdate()],
["Creditors - _TC", 0.0, 247.5, nowdate()],
["Discount Account - _TC", 0.0, 27.5, nowdate()]
]
check_gl_entries(self, pi.name, expected_gle, nowdate())
def test_purchase_invoice_change_naming_series(self):
pi = frappe.copy_doc(test_records[1])
pi.insert()
@@ -1140,6 +1184,18 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(expected_gle[i][0], gle.account)
self.assertEqual(expected_gle[i][1], gle.amount)
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s
order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1)
for i, gle in enumerate(gl_entries):
doc.assertEqual(expected_gle[i][0], gle.account)
doc.assertEqual(expected_gle[i][1], gle.debit)
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
def update_tax_witholding_category(company, account, date):
from erpnext.accounts.utils import get_fiscal_year
@@ -1170,6 +1226,11 @@ def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings.unlink_payment_on_cancellation_of_invoice = enable
accounts_settings.save()
def enable_discount_accounting(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
accounts_settings.enable_discount_accounting = enable
accounts_settings.save()
def make_purchase_invoice(**args):
pi = frappe.new_doc("Purchase Invoice")
args = frappe._dict(args)
@@ -1192,6 +1253,7 @@ def make_purchase_invoice(**args):
pi.return_against = args.return_against
pi.is_subcontracted = args.is_subcontracted or "No"
pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC"
pi.cost_center = args.parent_cost_center
pi.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
@@ -1200,7 +1262,10 @@ def make_purchase_invoice(**args):
"received_qty": args.received_qty or 0,
"rejected_qty": args.rejected_qty or 0,
"rate": args.rate or 50,
'expense_account': args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"price_list_rate": args.price_list_rate or 50,
"expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC',
"discount_account": args.discount_account or None,
"discount_amount": args.discount_amount or 0,
"conversion_factor": 1.0,
"serial_no": args.serial_no,
"stock_uom": args.uom or "_Test UOM",

View File

@@ -73,6 +73,7 @@
"manufacturer_part_no",
"accounting",
"expense_account",
"discount_account",
"col_break5",
"is_fixed_asset",
"asset_location",
@@ -501,6 +502,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "enable_deferred_expense",
"fieldname": "deferred_expense_section",
"fieldtype": "Section Break",
"label": "Deferred Expense"
@@ -849,12 +851,18 @@
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "discount_account",
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-16 19:57:03.101571",
"modified": "2021-08-12 20:14:45.506639",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",

View File

@@ -22,7 +22,7 @@
"cost_center",
"dimension_col_break",
"section_break_9",
"currency",
"account_currency",
"tax_amount",
"tax_amount_after_discount_amount",
"total",
@@ -208,14 +208,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
@@ -223,12 +215,20 @@
"fieldname": "included_in_paid_amount",
"fieldtype": "Check",
"label": "Considered In Paid Amount"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-06-14 01:43:50.750455",
"modified": "2021-08-05 20:04:36.618240",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",

View File

@@ -347,7 +347,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
items_add: function(doc, cdt, cdn) {
var row = frappe.get_doc(cdt, cdn);
this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "cost_center"]);
this.frm.script_manager.copy_from_first_row("items", row, ["income_account", "discount_account", "cost_center"]);
},
set_dynamic_labels: function() {
@@ -447,6 +447,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
this.frm.refresh_field("outstanding_amount");
this.frm.refresh_field("paid_amount");
this.frm.refresh_field("base_paid_amount");
},
currency() {
this._super();
$.each(cur_frm.doc.timesheets, function(i, d) {
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
});
calculate_total_billing_amount(cur_frm)
}
});
@@ -510,7 +519,6 @@ cur_frm.set_query("income_account", "items", function(doc) {
}
});
// Cost Center in Details Table
// -----------------------------
cur_frm.fields_dict["items"].grid.get_field("cost_center").get_query = function(doc) {
@@ -592,6 +600,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
frm.set_query("additional_discount_account", function() {
return {
filters: {
company: frm.doc.company,
is_group: 0,
report_type: "Profit and Loss",
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',
@@ -618,6 +636,17 @@ frappe.ui.form.on('Sales Invoice', {
}
}
// discount account
frm.fields_dict['items'].grid.get_field('discount_account').get_query = function(doc) {
return {
filters: {
'report_type': 'Profit and Loss',
'company': doc.company,
"is_group": 0
}
}
}
frm.fields_dict['items'].grid.get_field('deferred_revenue_account').get_query = function(doc) {
return {
filters: {
@@ -874,6 +903,7 @@ frappe.ui.form.on('Sales Invoice', {
row.billing_hours = time_log.billing_hours;
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
row.timesheet_detail = time_log.name;
row.project_name = time_log.project_name;
frm.refresh_field("timesheets");
frm.trigger("calculate_timesheet_totals");
@@ -978,6 +1008,21 @@ frappe.ui.form.on("Sales Invoice Timesheet", {
});
var set_timesheet_detail_rate = function(cdt, cdn, currency, timelog) {
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_timesheet_detail_rate",
args: {
timelog: timelog,
currency: currency
},
callback: function(r) {
if (!r.exc && r.message) {
frappe.model.set_value(cdt, cdn, 'billing_amount', r.message);
}
}
});
}
var select_loyalty_program = function(frm, loyalty_programs) {
var dialog = new frappe.ui.Dialog({
title: __("Select Loyalty Program"),

File diff suppressed because it is too large Load Diff

View File

@@ -290,6 +290,8 @@ class SalesInvoice(SellingController):
self.update_time_sheet(None)
def on_cancel(self):
check_if_return_invoice_linked_with_payment_entry(self)
super(SalesInvoice, self).on_cancel()
self.check_sales_order_on_hold_or_close("sales_order")
@@ -480,7 +482,7 @@ class SalesInvoice(SellingController):
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
return
self.pos_profile = pos_profile.get('name')
pos = {}
@@ -845,6 +847,7 @@ class SalesInvoice(SellingController):
self.allocate_advance_taxes(gl_entries)
self.make_item_gl_entries(gl_entries)
self.make_discount_gl_entries(gl_entries)
# merge gl entries before adding pos entries
gl_entries = merge_similar_entries(gl_entries)
@@ -885,17 +888,19 @@ class SalesInvoice(SellingController):
def make_tax_gl_entries(self, gl_entries):
for tax in self.get("taxes"):
amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting)
if flt(tax.base_tax_amount_after_discount_amount):
account_currency = get_account_currency(tax.account_head)
gl_entries.append(
self.get_gl_dict({
"account": tax.account_head,
"against": self.customer,
"credit": flt(tax.base_tax_amount_after_discount_amount,
"credit": flt(base_amount,
tax.precision("tax_amount_after_discount_amount")),
"credit_in_account_currency": (flt(tax.base_tax_amount_after_discount_amount,
"credit_in_account_currency": (flt(base_amount,
tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else
flt(tax.tax_amount_after_discount_amount, tax.precision("tax_amount_after_discount_amount"))),
flt(amount, tax.precision("tax_amount_after_discount_amount"))),
"cost_center": tax.cost_center
}, account_currency, item=tax)
)
@@ -939,15 +944,17 @@ class SalesInvoice(SellingController):
income_account = (item.income_account
if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
amount, base_amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting)
account_currency = get_account_currency(income_account)
gl_entries.append(
self.get_gl_dict({
"account": income_account,
"against": self.customer,
"credit": flt(item.base_net_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
"credit": flt(base_amount, item.precision("base_net_amount")),
"credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount"))
if account_currency==self.company_currency
else flt(item.net_amount, item.precision("net_amount"))),
else flt(amount, item.precision("net_amount"))),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
@@ -958,6 +965,19 @@ class SalesInvoice(SellingController):
erpnext.is_perpetual_inventory_enabled(self.company):
gl_entries += super(SalesInvoice, self).get_gl_entries()
@property
def enable_discount_accounting(self):
if not hasattr(self, "_enable_discount_accounting"):
self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
return self._enable_discount_accounting
def set_asset_status(self, asset):
if self.is_return:
asset.set_status()
else:
asset.set_status("Sold" if self.docstatus==1 else None)
def make_loyalty_point_redemption_gle(self, gl_entries):
if cint(self.redeem_loyalty_points):
gl_entries.append(
@@ -1923,3 +1943,41 @@ def create_dunning(source_name, target_doc=None):
}
}, target_doc, set_missing_values)
return doclist
def check_if_return_invoice_linked_with_payment_entry(self):
# If a Return invoice is linked with payment entry along with other invoices,
# the cancellation of the Return causes allocated amount to be greater than paid
if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
return
payment_entries = []
if self.is_return and self.return_against:
invoice = self.return_against
else:
invoice = self.name
payment_entries = frappe.db.sql_list("""
SELECT
t1.name
FROM
`tabPayment Entry` t1, `tabPayment Entry Reference` t2
WHERE
t1.name = t2.parent
and t1.docstatus = 1
and t2.reference_name = %s
and t2.allocated_amount < 0
""", invoice)
links_to_pe = []
if payment_entries:
for payment in payment_entries:
payment_entry = frappe.get_doc("Payment Entry", payment)
if len(payment_entry.references) > 1:
links_to_pe.append(payment_entry.name)
if links_to_pe:
payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe]
message = _("Please cancel and amend the Payment Entry")
message += " " + ", ".join(payment_entries_link) + " "
message += _("to unallocate the amount of this Return Invoice before cancelling it.")
frappe.throw(message)

View File

@@ -1986,6 +1986,54 @@ class TestSalesInvoice(unittest.TestCase):
sales_invoice.save()
self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def test_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting
enable_discount_accounting()
discount_account = create_account(account_name="Discount Account",
parent_account="Indirect Expenses - _TC", company="_Test Company")
si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90)
expected_gle = [
["Debtors - _TC", 90.0, 0.0, nowdate()],
["Discount Account - _TC", 10.0, 0.0, nowdate()],
["Sales - _TC", 0.0, 100.0, nowdate()]
]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0)
def test_additional_discount_for_sales_invoice_with_discount_accounting_enabled(self):
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import enable_discount_accounting
enable_discount_accounting()
additional_discount_account = create_account(account_name="Discount Account",
parent_account="Indirect Expenses - _TC", company="_Test Company")
si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1)
si.apply_discount_on = "Grand Total"
si.additional_discount_account = additional_discount_account
si.additional_discount_percentage = 20
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "_Test Account VAT - _TC",
"cost_center": "Main - _TC",
"description": "Test",
"rate": 10
})
si.submit()
expected_gle = [
["_Test Account VAT - _TC", 0.0, 10.0, nowdate()],
["Debtors - _TC", 88, 0.0, nowdate()],
["Discount Account - _TC", 22.0, 0.0, nowdate()],
["Sales - _TC", 0.0, 100.0, nowdate()]
]
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
enable_discount_accounting(enable=0)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
@@ -2179,6 +2227,7 @@ def create_sales_invoice(**args):
si.currency=args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1
si.naming_series = args.naming_series or "T-SINV-"
si.cost_center = args.parent_cost_center
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
@@ -2190,8 +2239,11 @@ def create_sales_invoice(**args):
"uom": args.uom or "Nos",
"stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,
"price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"discount_account": args.discount_account or None,
"discount_amount": args.discount_amount or 0,
"cost_center": args.cost_center or "_Test Cost Center - _TC",
"serial_no": args.serial_no,
"conversion_factor": 1

View File

@@ -63,6 +63,7 @@
"finance_book",
"col_break4",
"expense_account",
"discount_account",
"deferred_revenue",
"deferred_revenue_account",
"service_stop_date",
@@ -473,6 +474,7 @@
},
{
"collapsible": 1,
"collapsible_depends_on": "enable_deferred_revenue",
"fieldname": "deferred_revenue",
"fieldtype": "Section Break",
"label": "Deferred Revenue"
@@ -821,12 +823,18 @@
"no_copy": 1,
"options": "currency",
"read_only": 1
},
{
"fieldname": "discount_account",
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-02-23 01:05:22.123527",
"modified": "2021-08-12 20:15:42.668399",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",

View File

@@ -16,8 +16,10 @@
"column_break_9",
"billing_amount",
"section_break_11",
"timesheet_detail",
"column_break_5",
"time_sheet",
"timesheet_detail"
"project_name"
],
"fields": [
{
@@ -89,6 +91,7 @@
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_7",
"fieldtype": "Section Break",
"label": "Totals"
@@ -101,11 +104,17 @@
"fieldname": "section_break_11",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "project_name",
"fieldtype": "Data",
"label": "Project Name",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-08-02 23:03:08.084930",
"modified": "2021-08-15 18:37:08.084930",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Timesheet",

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-06 16:17:44.329943",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sales_partner"
],
"fields": [
{
"fieldname": "sales_partner",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Partner ",
"options": "Sales Partner"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 10:43:37.532095",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Partner Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -19,7 +19,7 @@
"section_break_8",
"rate",
"section_break_9",
"currency",
"account_currency",
"tax_amount",
"total",
"tax_amount_after_discount_amount",
@@ -186,14 +186,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
},
{
"default": "0",
"depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
@@ -210,13 +202,21 @@
"label": "Dont Recompute tax",
"print_hide": 1,
"read_only": 1
},
{
"fetch_from": "account_head.account_currency",
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency",
"read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-07-27 12:40:59.051803",
"modified": "2021-08-05 20:04:01.726867",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",

View File

@@ -6,7 +6,7 @@ import frappe
from frappe import _
from frappe.utils import flt
from frappe.model.document import Document
from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax
from erpnext.controllers.accounts_controller import validate_taxes_and_charges, validate_inclusive_tax, validate_cost_center, validate_account_head
class SalesTaxesandChargesTemplate(Document):
def validate(self):
@@ -39,6 +39,8 @@ def valdiate_taxes_and_charges_template(doc):
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_account_head(tax, doc)
validate_cost_center(tax, doc)
validate_inclusive_tax(tax, doc)
def validate_disabled(doc):

View File

@@ -8,6 +8,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 6
},
@@ -16,6 +17,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 6.36
}
@@ -114,6 +116,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 12
},
@@ -122,6 +125,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 4
}
@@ -137,6 +141,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 12
},
@@ -145,6 +150,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 4
}
@@ -160,6 +166,7 @@
"charge_type": "On Net Total",
"description": "VAT",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 12
},
@@ -168,6 +175,7 @@
"charge_type": "On Net Total",
"description": "Service Tax",
"doctype": "Sales Taxes and Charges",
"cost_center": "Main - _TC",
"parentfield": "taxes",
"rate": 4
}

View File

@@ -78,7 +78,7 @@
"label": "Cost"
},
{
"depends_on": "eval:doc.price_determination==\"Based on price list\"",
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
"fieldname": "price_list",
"fieldtype": "Link",
"label": "Price List",
@@ -147,7 +147,7 @@
}
],
"links": [],
"modified": "2020-06-25 10:53:44.205774",
"modified": "2021-08-09 10:53:44.205774",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Plan",

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-06 16:19:22.040795",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"supplier_group"
],
"fields": [
{
"fieldname": "supplier_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier Group",
"options": "Supplier Group"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 10:43:59.877938",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Supplier Group Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-06 16:18:54.758468",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"supplier"
],
"fields": [
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 10:44:09.707778",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Supplier Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -0,0 +1,31 @@
{
"actions": [],
"creation": "2021-05-06 16:16:51.885441",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"territory"
],
"fields": [
{
"fieldname": "territory",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Territory",
"options": "Territory"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-07 10:43:26.641030",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Territory Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -100,8 +100,8 @@ def merge_similar_entries(gl_map, precision=None):
return merged_gl_map
def check_if_in_list(gle, gl_map, dimensions=None):
account_head_fieldnames = ['party_type', 'party', 'against_voucher', 'against_voucher_type',
'cost_center', 'project', 'voucher_detail_no']
account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher',
'cost_center', 'against_voucher_type', 'party_type', 'project']
if dimensions:
account_head_fieldnames = account_head_fieldnames + dimensions
@@ -110,10 +110,12 @@ def check_if_in_list(gle, gl_map, dimensions=None):
same_head = True
if e.account != gle.account:
same_head = False
continue
for fieldname in account_head_fieldnames:
if cstr(e.get(fieldname)) != cstr(gle.get(fieldname)):
same_head = False
break
if same_head:
return e
@@ -143,16 +145,19 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
validate_expense_against_budget(args)
def validate_cwip_accounts(gl_map):
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
"""Validate that CWIP account are not used in Journal Entry"""
if gl_map and gl_map[0].voucher_type != "Journal Entry":
return
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")]
cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting"))
if cwip_enabled:
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
where account_type = 'Capital Work in Progress' and is_group=0""")]
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
for entry in gl_map:
if entry.account in cwip_accounts:
frappe.throw(
_("Account: <b>{0}</b> is capital Work in progress and can not be updated by Journal Entry").format(entry.account))
def round_off_debit_credit(gl_map):
precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),

View File

@@ -8,7 +8,7 @@ from frappe import _, msgprint, scrub
from frappe.core.doctype.user_permission.user_permission import get_permitted_documents
from frappe.model.utils import get_fetch_values
from frappe.utils import (add_days, getdate, formatdate, date_diff,
add_years, get_timestamp, nowdate, flt, cstr, add_months, get_last_day)
add_years, get_timestamp, nowdate, flt, cstr, add_months, get_last_day, cint)
from frappe.contacts.doctype.address.address import (get_address_display,
get_default_address, get_company_address)
from frappe.contacts.doctype.contact.contact import get_contact_details
@@ -58,7 +58,7 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category,
billing_address=party_address, shipping_address=shipping_address)
if fetch_payment_terms_template:
if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)
if not party_details.get("currency"):

View File

@@ -920,7 +920,6 @@ def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, wa
_delete_gl_entries(voucher_type, voucher_no)
def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = []
values = []
condition = ""
@@ -936,30 +935,46 @@ def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, f
condition += " and company = %s"
values.append(company)
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
future_stock_vouchers = frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
where
timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
and is_cancelled = 0
{condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
tuple([posting_date, posting_time] + values), as_dict=True)
return future_stock_vouchers
return [(d.voucher_type, d.voucher_no) for d in future_stock_vouchers]
def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
""" Get voucherwise list of GL entries.
Only fetches GLE fields required for comparing with new GLE.
Check compare_existing_and_expected_gle function below.
"""
gl_entries = {}
if future_stock_vouchers:
for d in frappe.db.sql("""select * from `tabGL Entry`
where posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s']*len(future_stock_vouchers))),
tuple([posting_date] + [d[1] for d in future_stock_vouchers]), as_dict=1):
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
if not future_stock_vouchers:
return gl_entries
voucher_nos = [d[1] for d in future_stock_vouchers]
gles = frappe.db.sql("""
select name, account, credit, debit, cost_center, project
from `tabGL Entry`
where
posting_date >= %s and voucher_no in (%s)""" %
('%s', ', '.join(['%s'] * len(voucher_nos))),
tuple([posting_date] + voucher_nos), as_dict=1)
for d in gles:
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
if len(existing_gle) != len(expected_gle):
return False
matched = True
for entry in expected_gle:
account_existed = False

View File

@@ -82,24 +82,46 @@ frappe.ui.form.on('Asset', {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() {
erpnext.asset.transfer_asset(frm);
});
}, __("Manage"));
frm.add_custom_button("Scrap Asset", function() {
erpnext.asset.scrap_asset(frm);
});
}, __("Manage"));
frm.add_custom_button("Sell Asset", function() {
frm.trigger("make_sales_invoice");
});
}, __("Manage"));
} else if (frm.doc.status=='Scrapped') {
frm.add_custom_button("Restore Asset", function() {
erpnext.asset.restore_asset(frm);
});
}, __("Manage"));
}
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(__("Maintain Asset"), function() {
frm.trigger("create_asset_maintenance");
}, __("Manage"));
}
frm.add_custom_button(__("Repair Asset"), function() {
frm.trigger("create_asset_repair");
}, __("Manage"));
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment");
}, __("Manage"));
}
if (!frm.doc.calculate_depreciation) {
frm.add_custom_button(__("Create Depreciation Entry"), function() {
frm.trigger("make_journal_entry");
}, __("Manage"));
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button("General Ledger", function() {
frm.add_custom_button("View General Ledger", function() {
frappe.route_options = {
"voucher_no": frm.doc.name,
"from_date": frm.doc.available_for_use_date,
@@ -107,27 +129,9 @@ frappe.ui.form.on('Asset', {
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
});
}, __("Manage"));
}
if (frm.doc.maintenance_required && !frm.doc.maintenance_schedule) {
frm.add_custom_button(__("Asset Maintenance"), function() {
frm.trigger("create_asset_maintenance");
}, __('Create'));
}
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Asset Value Adjustment"), function() {
frm.trigger("create_asset_adjustment");
}, __('Create'));
}
if (!frm.doc.calculate_depreciation) {
frm.add_custom_button(__("Depreciation Entry"), function() {
frm.trigger("make_journal_entry");
}, __('Create'));
}
frm.page.set_inner_btn_group_as_primary(__('Create'));
frm.trigger("setup_chart");
}
@@ -304,6 +308,20 @@ frappe.ui.form.on('Asset', {
})
},
create_asset_repair: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"asset_name": frm.doc.asset_name
},
method: "erpnext.assets.doctype.asset.asset.create_asset_repair",
callback: function(r) {
var doclist = frappe.model.sync(r.message);
frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
}
});
},
create_asset_adjustment: function(frm) {
frappe.call({
args: {

View File

@@ -502,7 +502,7 @@
"link_fieldname": "asset"
}
],
"modified": "2021-01-22 12:38:59.091510",
"modified": "2021-06-24 14:58:51.097908",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",

View File

@@ -168,17 +168,24 @@ class Asset(AccountsController):
d.precision("rate_of_depreciation"))
def make_depreciation_schedule(self):
if 'Manual' not in [d.depreciation_method for d in self.finance_books]:
if 'Manual' not in [d.depreciation_method for d in self.finance_books] and not self.schedules:
self.schedules = []
if self.get("schedules") or not self.available_for_use_date:
if not self.available_for_use_date:
return
for d in self.get('finance_books'):
self.validate_asset_finance_books(d)
start = self.clear_depreciation_schedule()
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
# value_after_depreciation - current Asset value
if d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) -
flt(self.opening_accumulated_depreciation))
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
@@ -191,7 +198,7 @@ class Asset(AccountsController):
number_of_pending_depreciations += 1
skip_row = False
for n in range(number_of_pending_depreciations):
for n in range(start, number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
@@ -216,11 +223,13 @@ class Asset(AccountsController):
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation))
if not self.flags.increase_in_asset_life:
# In case of increase_in_asset_life, the self.to_date is already set on asset_repair submission
self.to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation))
depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, schedule_date, to_date)
depreciation_amount, schedule_date, self.to_date)
monthly_schedule_date = add_months(schedule_date, 1)
@@ -284,10 +293,23 @@ class Asset(AccountsController):
"finance_book_id": d.idx
})
# used when depreciation schedule needs to be modified due to increase in asset life
def clear_depreciation_schedule(self):
start = 0
for n in range(len(self.schedules)):
if not self.schedules[n].journal_entry:
del self.schedules[n:]
start = n
break
return start
# if it returns True, depreciation_amount will not be equal for the first and last rows
def check_is_pro_rata(self, row):
has_pro_rata = False
days = date_diff(row.depreciation_start_date, self.available_for_use_date) + 1
# if frequency_of_depreciation is 12 months, total_days = 365
total_days = get_total_days(row.depreciation_start_date, row.frequency_of_depreciation)
if days < total_days:
@@ -346,11 +368,12 @@ class Asset(AccountsController):
if d.finance_book_id not in finance_books:
accumulated_depreciation = flt(self.opening_accumulated_depreciation)
value_after_depreciation = flt(self.get_value_after_depreciation(d.finance_book_id))
finance_books.append(d.finance_book_id)
finance_books.append(int(d.finance_book_id))
depreciation_amount = flt(d.depreciation_amount, d.precision("depreciation_amount"))
value_after_depreciation -= flt(depreciation_amount)
# for the last row, if depreciation method = Straight Line
if straight_line_idx and i == max(straight_line_idx) - 1:
book = self.get('finance_books')[cint(d.finance_book_id) - 1]
depreciation_amount += flt(value_after_depreciation -
@@ -625,9 +648,18 @@ def create_asset_maintenance(asset, item_code, item_name, asset_category, compan
})
return asset_maintenance
@frappe.whitelist()
def create_asset_repair(asset, asset_name):
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({
"asset": asset,
"asset_name": asset_name
})
return asset_repair
@frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company):
asset_maintenance = frappe.new_doc("Asset Value Adjustment")
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({
"asset": asset,
"company": company,
@@ -757,9 +789,16 @@ def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
# if the Depreciation Schedule is being modified after Asset Repair
else:
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / (date_diff(asset.to_date, asset.available_for_use_date) / 365)
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount
return depreciation_amount

View File

@@ -125,7 +125,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
expected_schedules = [
@@ -154,9 +153,8 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12,
"depreciation_start_date": '2030-12-31'
})
asset.insert()
self.assertEqual(asset.status, "Draft")
asset.save()
self.assertEqual(asset.status, "Draft")
expected_schedules = [
['2030-12-31', 66667.00, 66667.00],
@@ -185,7 +183,7 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
asset.insert()
asset.save()
self.assertEqual(asset.status, "Draft")
expected_schedules = [
@@ -216,7 +214,6 @@ class TestAsset(unittest.TestCase):
"depreciation_start_date": "2030-12-31"
})
asset.insert()
asset.save()
expected_schedules = [
@@ -247,7 +244,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
asset.insert()
asset.submit()
asset.load_from_db()
self.assertEqual(asset.status, "Submitted")
@@ -350,7 +346,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -380,7 +375,6 @@ class TestAsset(unittest.TestCase):
"total_number_of_depreciations": 10,
"frequency_of_depreciation": 1
})
asset.insert()
asset.submit()
post_depreciation_entries(date=add_months('2020-01-01', 4))
@@ -424,7 +418,6 @@ class TestAsset(unittest.TestCase):
"frequency_of_depreciation": 10,
"depreciation_start_date": "2020-12-31"
})
asset.insert()
asset.submit()
post_depreciation_entries(date="2021-01-01")
@@ -468,7 +461,7 @@ class TestAsset(unittest.TestCase):
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 10
})
asset.insert()
asset.save()
accumulated_depreciation_after_full_schedule = \
max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
@@ -646,7 +639,7 @@ class TestAsset(unittest.TestCase):
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-12'
asset.available_for_use_date = '2030-07-12'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 1000,
@@ -660,10 +653,10 @@ class TestAsset(unittest.TestCase):
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 1106.85, 1106.85],
["2031-12-31", 3446.58, 4553.43],
["2032-12-31", 1723.29, 6276.72],
["2033-06-12", 723.28, 7000.00]
["2030-12-31", 942.47, 942.47],
["2031-12-31", 3528.77, 4471.24],
["2032-12-31", 1764.38, 6235.62],
["2033-07-12", 764.38, 7000.00]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
@@ -699,7 +692,7 @@ def create_asset(**args):
"item_code": args.item_code or "Macbook Pro",
"company": args.company or"_Test Company",
"purchase_date": "2015-01-01",
"calculate_depreciation": 0,
"calculate_depreciation": args.calculate_depreciation or 0,
"gross_purchase_amount": 100000,
"purchase_receipt_amount": 100000,
"expected_value_after_useful_life": 10000,
@@ -707,9 +700,16 @@ def create_asset(**args):
"available_for_use_date": "2020-06-06",
"location": "Test Location",
"asset_owner": "Company",
"is_existing_asset": args.is_existing_asset or 0
"is_existing_asset": 1
})
if asset.calculate_depreciation:
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 5
})
try:
asset.save()
except frappe.DuplicateEntryError:

View File

@@ -67,7 +67,6 @@
{
"fieldname": "value_after_depreciation",
"fieldtype": "Currency",
"hidden": 1,
"label": "Value After Depreciation",
"no_copy": 1,
"options": "Company:company:default_currency",
@@ -85,7 +84,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2020-11-05 16:30:09.213479",
"modified": "2021-06-17 12:59:05.743683",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Finance Book",

View File

@@ -2,6 +2,45 @@
// For license information, please see license.txt
frappe.ui.form.on('Asset Repair', {
setup: function(frm) {
frm.fields_dict.cost_center.get_query = function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
};
frm.fields_dict.project.get_query = function(doc) {
return {
filters: {
'company': doc.company
}
};
};
frm.fields_dict.warehouse.get_query = function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
};
},
refresh: function(frm) {
if (frm.doc.docstatus) {
frm.add_custom_button("View General Ledger", function() {
frappe.route_options = {
"voucher_no": frm.doc.name
};
frappe.set_route("query-report", "General Ledger");
});
}
},
repair_status: (frm) => {
if (frm.doc.completion_date && frm.doc.repair_status == "Completed") {
frappe.call ({
@@ -17,5 +56,16 @@ frappe.ui.form.on('Asset Repair', {
}
});
}
if (frm.doc.repair_status == "Completed") {
frm.set_value('completion_date', frappe.datetime.now_datetime());
}
}
});
frappe.ui.form.on('Asset Repair Consumed Item', {
consumed_quantity: function(frm, cdt, cdn) {
var row = locals[cdt][cdn];
frappe.model.set_value(cdt, cdn, 'total_value', row.consumed_quantity * row.valuation_rate);
},
});

View File

@@ -7,38 +7,43 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"asset_name",
"asset",
"company",
"column_break_2",
"item_code",
"item_name",
"asset_name",
"naming_series",
"section_break_5",
"failure_date",
"assign_to",
"assign_to_name",
"repair_status",
"column_break_6",
"completion_date",
"repair_status",
"accounting_dimensions_section",
"cost_center",
"column_break_14",
"project",
"accounting_details",
"repair_cost",
"capitalize_repair_cost",
"stock_consumption",
"column_break_8",
"purchase_invoice",
"stock_consumption_details_section",
"warehouse",
"stock_items",
"total_repair_cost",
"stock_entry",
"asset_depreciation_details_section",
"increase_in_asset_life",
"section_break_9",
"description",
"column_break_9",
"actions_performed",
"section_break_17",
"section_break_23",
"downtime",
"column_break_19",
"amended_from"
],
"fields": [
{
"columns": 1,
"fieldname": "asset_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
@@ -50,18 +55,6 @@
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"fetch_from": "asset_name.item_code",
"fieldname": "item_code",
"fieldtype": "Read Only",
"label": "Item Code"
},
{
"fetch_from": "asset_name.item_name",
"fieldname": "item_name",
"fieldtype": "Read Only",
"label": "Item Name"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
@@ -74,33 +67,20 @@
"label": "Failure Date",
"reqd": 1
},
{
"allow_on_submit": 1,
"fieldname": "assign_to",
"fieldtype": "Link",
"label": "Assign To",
"options": "User"
},
{
"allow_on_submit": 1,
"fetch_from": "assign_to.full_name",
"fieldname": "assign_to_name",
"fieldtype": "Read Only",
"label": "Assign To Name"
},
{
"fieldname": "column_break_6",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"depends_on": "eval:!doc.__islocal",
"fieldname": "completion_date",
"fieldtype": "Datetime",
"label": "Completion Date"
"label": "Completion Date",
"no_copy": 1
},
{
"allow_on_submit": 1,
"default": "Pending",
"depends_on": "eval:!doc.__islocal",
"fieldname": "repair_status",
"fieldtype": "Select",
"label": "Repair Status",
@@ -116,25 +96,18 @@
{
"fieldname": "description",
"fieldtype": "Long Text",
"label": "Error Description",
"reqd": 1
"label": "Error Description"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"fieldname": "actions_performed",
"fieldtype": "Long Text",
"label": "Actions performed"
},
{
"fieldname": "section_break_17",
"fieldtype": "Section Break"
},
{
"allow_on_submit": 1,
"fieldname": "downtime",
"fieldtype": "Data",
"in_list_view": 1,
@@ -146,7 +119,7 @@
"fieldtype": "Column Break"
},
{
"allow_on_submit": 1,
"default": "0",
"fieldname": "repair_cost",
"fieldtype": "Currency",
"label": "Repair Cost"
@@ -159,12 +132,138 @@
"options": "Asset Repair",
"print_hide": 1,
"read_only": 1
},
{
"columns": 1,
"fieldname": "asset",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Asset",
"options": "Asset",
"reqd": 1
},
{
"fetch_from": "asset.asset_name",
"fieldname": "asset_name",
"fieldtype": "Read Only",
"label": "Asset Name"
},
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:!doc.__islocal",
"fieldname": "capitalize_repair_cost",
"fieldtype": "Check",
"label": "Capitalize Repair Cost"
},
{
"fieldname": "accounting_details",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "stock_items",
"fieldtype": "Table",
"label": "Stock Items",
"mandatory_depends_on": "stock_consumption",
"options": "Asset Repair Consumed Item"
},
{
"fieldname": "section_break_23",
"fieldtype": "Section Break"
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
},
{
"default": "0",
"depends_on": "eval:!doc.__islocal",
"fieldname": "stock_consumption",
"fieldtype": "Check",
"label": "Stock Consumed During Repair"
},
{
"depends_on": "stock_consumption",
"fieldname": "stock_consumption_details_section",
"fieldtype": "Section Break",
"label": "Stock Consumption Details"
},
{
"depends_on": "eval: doc.stock_consumption && doc.total_repair_cost > 0",
"description": "Sum of Repair Cost and Value of Consumed Stock Items.",
"fieldname": "total_repair_cost",
"fieldtype": "Currency",
"label": "Total Repair Cost",
"read_only": 1
},
{
"depends_on": "stock_consumption",
"fieldname": "warehouse",
"fieldtype": "Link",
"label": "Warehouse",
"options": "Warehouse"
},
{
"depends_on": "capitalize_repair_cost",
"fieldname": "asset_depreciation_details_section",
"fieldtype": "Section Break",
"label": "Asset Depreciation Details"
},
{
"fieldname": "increase_in_asset_life",
"fieldtype": "Int",
"label": "Increase In Asset Life(Months)",
"no_copy": 1
},
{
"depends_on": "eval:!doc.__islocal",
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
"mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0",
"no_copy": 1,
"options": "Purchase Invoice"
},
{
"fetch_from": "asset.company",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "stock_entry",
"fieldtype": "Link",
"label": "Stock Entry",
"options": "Stock Entry",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-01-22 15:08:12.495850",
"modified": "2021-06-25 13:14:38.307723",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair",
@@ -203,6 +302,7 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "asset_name",
"track_changes": 1,
"track_seen": 1
}

View File

@@ -5,16 +5,252 @@
from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import time_diff_in_hours
from frappe.model.document import Document
from frappe.utils import time_diff_in_hours, getdate, add_months, flt, cint
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.assets.doctype.asset.asset import get_asset_account
from erpnext.controllers.accounts_controller import AccountsController
class AssetRepair(Document):
class AssetRepair(AccountsController):
def validate(self):
if self.repair_status == "Completed" and not self.completion_date:
frappe.throw(_("Please select Completion Date for Completed Repair"))
self.asset_doc = frappe.get_doc('Asset', self.asset)
self.update_status()
if self.get('stock_items'):
self.set_total_value()
self.calculate_total_repair_cost()
def update_status(self):
if self.repair_status == 'Pending':
frappe.db.set_value('Asset', self.asset, 'status', 'Out of Order')
else:
self.asset_doc.set_status()
def set_total_value(self):
for item in self.get('stock_items'):
item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
def calculate_total_repair_cost(self):
self.total_repair_cost = flt(self.repair_cost)
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
self.total_repair_cost += total_value_of_stock_consumed
def before_submit(self):
self.check_repair_status()
if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
self.increase_asset_value()
if self.get('stock_consumption'):
self.check_for_stock_items_and_warehouse()
self.decrease_stock_quantity()
if self.get('capitalize_repair_cost'):
self.make_gl_entries()
if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
self.modify_depreciation_schedule()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
def before_cancel(self):
self.asset_doc = frappe.get_doc('Asset', self.asset)
if self.get('stock_consumption') or self.get('capitalize_repair_cost'):
self.decrease_asset_value()
if self.get('stock_consumption'):
self.increase_stock_quantity()
if self.get('capitalize_repair_cost'):
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
self.make_gl_entries(cancel=True)
if frappe.db.get_value('Asset', self.asset, 'calculate_depreciation') and self.increase_in_asset_life:
self.revert_depreciation_schedule_on_cancellation()
self.asset_doc.flags.ignore_validate_update_after_submit = True
self.asset_doc.prepare_depreciation_data()
self.asset_doc.save()
def check_repair_status(self):
if self.repair_status == "Pending":
frappe.throw(_("Please update Repair Status."))
def check_for_stock_items_and_warehouse(self):
if not self.get('stock_items'):
frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items"))
if not self.warehouse:
frappe.throw(_("Please enter Warehouse from which Stock Items consumed during the Repair were taken."), title=_("Missing Warehouse"))
def increase_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
row.value_after_depreciation += total_value_of_stock_consumed
if self.capitalize_repair_cost:
row.value_after_depreciation += self.repair_cost
def decrease_asset_value(self):
total_value_of_stock_consumed = self.get_total_value_of_stock_consumed()
if self.asset_doc.calculate_depreciation:
for row in self.asset_doc.finance_books:
row.value_after_depreciation -= total_value_of_stock_consumed
if self.capitalize_repair_cost:
row.value_after_depreciation -= self.repair_cost
def get_total_value_of_stock_consumed(self):
total_value_of_stock_consumed = 0
if self.get('stock_consumption'):
for item in self.get('stock_items'):
total_value_of_stock_consumed += item.total_value
return total_value_of_stock_consumed
def decrease_stock_quantity(self):
stock_entry = frappe.get_doc({
"doctype": "Stock Entry",
"stock_entry_type": "Material Issue",
"company": self.company
})
for stock_item in self.get('stock_items'):
stock_entry.append('items', {
"s_warehouse": self.warehouse,
"item_code": stock_item.item,
"qty": stock_item.consumed_quantity,
"basic_rate": stock_item.valuation_rate
})
stock_entry.insert()
stock_entry.submit()
self.db_set('stock_entry', stock_entry.name)
def increase_stock_quantity(self):
stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
stock_entry.flags.ignore_links = True
stock_entry.cancel()
def make_gl_entries(self, cancel=False):
if flt(self.repair_cost) > 0:
gl_entries = self.get_gl_entries()
make_gl_entries(gl_entries, cancel)
def get_gl_entries(self):
gl_entries = []
repair_and_maintenance_account = frappe.db.get_value('Company', self.company, 'repair_and_maintenance_account')
fixed_asset_account = get_asset_account("fixed_asset_account", asset=self.asset, company=self.company)
expense_account = frappe.get_doc('Purchase Invoice', self.purchase_invoice).items[0].expense_account
gl_entries.append(
self.get_gl_dict({
"account": expense_account,
"credit": self.repair_cost,
"credit_in_account_currency": self.repair_cost,
"against": repair_and_maintenance_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company
}, item=self)
)
if self.get('stock_consumption'):
# creating GL Entries for each row in Stock Items based on the Stock Entry created for it
stock_entry = frappe.get_doc('Stock Entry', self.stock_entry)
for item in stock_entry.items:
gl_entries.append(
self.get_gl_dict({
"account": item.expense_account,
"credit": item.amount,
"credit_in_account_currency": item.amount,
"against": repair_and_maintenance_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"company": self.company
}, item=self)
)
gl_entries.append(
self.get_gl_dict({
"account": fixed_asset_account,
"debit": self.total_repair_cost,
"debit_in_account_currency": self.total_repair_cost,
"against": expense_account,
"voucher_type": self.doctype,
"voucher_no": self.name,
"cost_center": self.cost_center,
"posting_date": getdate(),
"against_voucher_type": "Purchase Invoice",
"against_voucher": self.purchase_invoice,
"company": self.company
}, item=self)
)
return gl_entries
def modify_depreciation_schedule(self):
for row in self.asset_doc.finance_books:
row.total_number_of_depreciations += self.increase_in_asset_life/row.frequency_of_depreciation
self.asset_doc.flags.increase_in_asset_life = False
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
if extra_months != 0:
self.calculate_last_schedule_date(self.asset_doc, row, extra_months)
# to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation
def calculate_last_schedule_date(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
cint(asset.number_of_depreciations_booked)
# the Schedule Date in the final row of the old Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
# the Schedule Date in the final row of the new Depreciation Schedule
asset.to_date = add_months(last_schedule_date, extra_months)
# the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations
# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
schedule_date = add_months(row.depreciation_start_date,
number_of_pending_depreciations * cint(row.frequency_of_depreciation))
if asset.to_date > schedule_date:
row.total_number_of_depreciations += 1
def revert_depreciation_schedule_on_cancellation(self):
for row in self.asset_doc.finance_books:
row.total_number_of_depreciations -= self.increase_in_asset_life/row.frequency_of_depreciation
self.asset_doc.flags.increase_in_asset_life = False
extra_months = self.increase_in_asset_life % row.frequency_of_depreciation
if extra_months != 0:
self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months)
def calculate_last_schedule_date_before_modification(self, asset, row, extra_months):
asset.flags.increase_in_asset_life = True
number_of_pending_depreciations = cint(row.total_number_of_depreciations) - \
cint(asset.number_of_depreciations_booked)
# the Schedule Date in the final row of the modified Depreciation Schedule
last_schedule_date = asset.schedules[len(asset.schedules)-1].schedule_date
# the Schedule Date in the final row of the original Depreciation Schedule
asset.to_date = add_months(last_schedule_date, -extra_months)
# the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations
# if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022...
schedule_date = add_months(row.depreciation_start_date,
(number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation))
if asset.to_date < schedule_date:
row.total_number_of_depreciations -= 1
@frappe.whitelist()
def get_downtime(failure_date, completion_date):
downtime = time_diff_in_hours(completion_date, failure_date)
return round(downtime, 2)
return round(downtime, 2)

View File

@@ -2,8 +2,167 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import nowdate, flt
import unittest
from erpnext.assets.doctype.asset.test_asset import create_asset_data, create_asset, set_depreciation_settings_in_company
class TestAssetRepair(unittest.TestCase):
pass
def setUp(self):
set_depreciation_settings_in_company()
create_asset_data()
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
asset = create_asset()
initial_status = asset.status
asset_repair = create_asset_repair(asset = asset)
if asset_repair.repair_status == "Pending":
asset.reload()
self.assertEqual(asset.status, "Out of Order")
asset_repair.repair_status = "Completed"
asset_repair.save()
asset_status = frappe.db.get_value("Asset", asset_repair.asset, "status")
self.assertEqual(asset_status, initial_status)
def test_stock_item_total_value(self):
asset_repair = create_asset_repair(stock_consumption = 1)
for item in asset_repair.stock_items:
total_value = flt(item.valuation_rate) * flt(item.consumed_quantity)
self.assertEqual(item.total_value, total_value)
def test_total_repair_cost(self):
asset_repair = create_asset_repair(stock_consumption = 1)
total_repair_cost = asset_repair.repair_cost
self.assertEqual(total_repair_cost, asset_repair.repair_cost)
for item in asset_repair.stock_items:
total_repair_cost += item.total_value
self.assertEqual(total_repair_cost, asset_repair.total_repair_cost)
def test_repair_status_after_submit(self):
asset_repair = create_asset_repair(submit = 1)
self.assertNotEqual(asset_repair.repair_status, "Pending")
def test_stock_items(self):
asset_repair = create_asset_repair(stock_consumption = 1)
self.assertTrue(asset_repair.stock_consumption)
self.assertTrue(asset_repair.stock_items)
def test_warehouse(self):
asset_repair = create_asset_repair(stock_consumption = 1)
self.assertTrue(asset_repair.stock_consumption)
self.assertTrue(asset_repair.warehouse)
def test_decrease_stock_quantity(self):
asset_repair = create_asset_repair(stock_consumption = 1, submit = 1)
stock_entry = frappe.get_last_doc('Stock Entry')
self.assertEqual(stock_entry.stock_entry_type, "Material Issue")
self.assertEqual(stock_entry.items[0].s_warehouse, asset_repair.warehouse)
self.assertEqual(stock_entry.items[0].item_code, asset_repair.stock_items[0].item)
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
def test_increase_in_asset_value_due_to_stock_consumption(self):
asset = create_asset(calculate_depreciation = 1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
asset.reload()
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
asset = create_asset(calculate_depreciation = 1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
increase_in_asset_value = get_asset_value(asset) - initial_asset_value
self.assertEqual(asset_repair.repair_cost, increase_in_asset_value)
def test_purchase_invoice(self):
asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
self.assertTrue(asset_repair.purchase_invoice)
def test_gl_entries(self):
asset_repair = create_asset_repair(capitalize_repair_cost = 1, submit = 1)
gl_entry = frappe.get_last_doc('GL Entry')
self.assertEqual(asset_repair.name, gl_entry.voucher_no)
def test_increase_in_asset_life(self):
asset = create_asset(calculate_depreciation = 1)
initial_num_of_depreciations = num_of_depreciations(asset)
create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
self.assertEqual((initial_num_of_depreciations + 1), num_of_depreciations(asset))
self.assertEqual(asset.schedules[-1].accumulated_depreciation_amount, asset.finance_books[0].value_after_depreciation)
def get_asset_value(asset):
return asset.finance_books[0].value_after_depreciation
def num_of_depreciations(asset):
return asset.finance_books[0].total_number_of_depreciations
def create_asset_repair(**args):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
args = frappe._dict(args)
if args.asset:
asset = args.asset
else:
asset = create_asset(is_existing_asset = 1)
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({
"asset": asset.name,
"asset_name": asset.asset_name,
"failure_date": nowdate(),
"description": "Test Description",
"repair_cost": 0,
"company": asset.company
})
if args.stock_consumption:
asset_repair.stock_consumption = 1
asset_repair.warehouse = create_warehouse("Test Warehouse", company = asset.company)
asset_repair.append("stock_items", {
"item": args.item or args.item_code or "_Test Item",
"valuation_rate": args.rate if args.get("rate") is not None else 100,
"consumed_quantity": args.qty or 1
})
asset_repair.insert(ignore_if_duplicate=True)
if args.submit:
asset_repair.repair_status = "Completed"
asset_repair.cost_center = "_Test Cost Center - _TC"
if args.stock_consumption:
stock_entry = frappe.get_doc({
"doctype": "Stock Entry",
"stock_entry_type": "Material Receipt",
"company": asset.company
})
stock_entry.append('items', {
"t_warehouse": asset_repair.warehouse,
"item_code": asset_repair.stock_items[0].item,
"qty": asset_repair.stock_items[0].consumed_quantity
})
stock_entry.submit()
if args.capitalize_repair_cost:
asset_repair.capitalize_repair_cost = 1
asset_repair.repair_cost = 1000
if asset.calculate_depreciation:
asset_repair.increase_in_asset_life = 12
asset_repair.purchase_invoice = make_purchase_invoice().name
asset_repair.submit()
return asset_repair

View File

@@ -0,0 +1,55 @@
{
"actions": [],
"creation": "2021-05-12 02:41:54.161024",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item",
"valuation_rate",
"consumed_quantity",
"total_value"
],
"fields": [
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{
"fetch_from": "item.valuation_rate",
"fieldname": "valuation_rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Valuation Rate",
"read_only": 1
},
{
"fieldname": "consumed_quantity",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Consumed Quantity"
},
{
"fieldname": "total_value",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Total Value",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-12 03:19:55.006300",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Repair Consumed Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@@ -447,10 +447,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
target.flags.ignore_permissions = ignore_permissions
set_missing_values(source, target)
#Get the advance paid Journal Entries in Purchase Invoice Advance
if target.get("allocate_advances_automatically"):
target.set_advances()
target.set_payment_schedule()
def update_item(obj, target, source_parent):
target.amount = flt(obj.amount) - flt(obj.billed_amt)
target.base_amount = target.amount * flt(source_parent.conversion_rate)
@@ -470,6 +471,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
"party_account_currency": "party_account_currency",
"supplier_warehouse":"supplier_warehouse"
},
"field_no_map" : ["payment_terms_template"],
"validation": {
"docstatus": ["=", 1],
}
@@ -489,12 +491,6 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
},
}
if frappe.get_single("Accounts Settings").automatically_fetch_payment_terms == 1:
fields["Payment Schedule"] = {
"doctype": "Payment Schedule",
"add_if_empty": True
}
doc = get_mapped_doc("Purchase Order", source_name, fields,
target_doc, postprocess, ignore_permissions=ignore_permissions)

View File

@@ -484,6 +484,9 @@ class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_invoice_with_terms(self):
from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules
automatically_fetch_payment_terms()
po = create_purchase_order(do_not_save=True)
self.assertRaises(frappe.ValidationError, make_pi_from_po, po.name)
@@ -509,6 +512,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(getdate(pi.payment_schedule[0].due_date), getdate(po.transaction_date))
self.assertEqual(pi.payment_schedule[1].payment_amount, 2500.0)
self.assertEqual(getdate(pi.payment_schedule[1].due_date), add_days(getdate(po.transaction_date), 30))
automatically_fetch_payment_terms(enable=0)
def test_subcontracting(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
@@ -632,14 +636,18 @@ class TestPurchaseOrder(unittest.TestCase):
else:
raise Exception
def test_terms_does_not_copy(self):
po = create_purchase_order()
self.assertTrue(po.get('payment_schedule'))
def test_terms_are_not_copied_if_automatically_fetch_payment_terms_is_unchecked(self):
po = create_purchase_order(do_not_save=1)
po.payment_terms_template = '_Test Payment Term Template'
po.save()
po.submit()
frappe.db.set_value('Company', '_Test Company', 'payment_terms', '_Test Payment Term Template 1')
pi = make_pi_from_po(po.name)
pi.save()
self.assertFalse(pi.get('payment_schedule'))
self.assertEqual(pi.get('payment_terms_template'), '_Test Payment Term Template 1')
frappe.db.set_value('Company', '_Test Company', 'payment_terms', '')
def test_terms_copied(self):
po = create_purchase_order(do_not_save=1)
@@ -968,8 +976,27 @@ class TestPurchaseOrder(unittest.TestCase):
# To test if the PO does NOT have a Blanket Order
self.assertEqual(po_doc.items[0].blanket_order, None)
def test_payment_terms_are_fetched_when_creating_purchase_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import automatically_fetch_payment_terms, compare_payment_schedules
automatically_fetch_payment_terms()
po = create_purchase_order(qty=10, rate=100, do_not_save=1)
create_payment_terms_template()
po.payment_terms_template = 'Test Receivable Template'
po.submit()
pi = make_purchase_invoice(qty=10, rate=100, do_not_save=1)
pi.items[0].purchase_order = po.name
pi.items[0].po_detail = po.items[0].name
pi.insert()
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
compare_payment_schedules(self, po, pi)
automatically_fetch_payment_terms(enable=0)
def make_pr_against_po(po, received_qty=0):
pr = make_purchase_receipt(po)

View File

@@ -813,6 +813,89 @@ class AccountsController(TransactionBase):
tax_map[tax.account_head] -= allocated_amount
allocated_tax_map[tax.account_head] -= allocated_amount
def get_amount_and_base_amount(self, item, enable_discount_accounting):
amount = item.net_amount
base_amount = item.base_net_amount
if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account'):
amount = item.amount
base_amount = item.base_amount
return amount, base_amount
def get_tax_amounts(self, tax, enable_discount_accounting):
amount = tax.tax_amount_after_discount_amount
base_amount = tax.base_tax_amount_after_discount_amount
if enable_discount_accounting and self.get('discount_amount') and self.get('additional_discount_account') \
and self.get('apply_discount_on') == 'Grand Total':
amount = tax.tax_amount
base_amount = tax.base_tax_amount
return amount, base_amount
def make_discount_gl_entries(self, gl_entries):
enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting'))
if enable_discount_accounting:
if self.doctype == "Purchase Invoice":
dr_or_cr = "credit"
rev_dr_cr = "debit"
supplier_or_customer = self.supplier
else:
dr_or_cr = "debit"
rev_dr_cr = "credit"
supplier_or_customer = self.customer
for item in self.get("items"):
if item.get('discount_amount') and item.get('discount_account'):
discount_amount = item.discount_amount * item.qty
if self.doctype == "Purchase Invoice":
income_or_expense_account = (item.expense_account
if (not item.enable_deferred_expense or self.is_return)
else item.deferred_expense_account)
else:
income_or_expense_account = (item.income_account
if (not item.enable_deferred_revenue or self.is_return)
else item.deferred_revenue_account)
account_currency = get_account_currency(item.discount_account)
gl_entries.append(
self.get_gl_dict({
"account": item.discount_account,
"against": supplier_or_customer,
dr_or_cr: flt(discount_amount, item.precision('discount_amount')),
dr_or_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'),
item.precision('discount_amount')),
"cost_center": item.cost_center,
"project": item.project
}, account_currency, item=item)
)
account_currency = get_account_currency(income_or_expense_account)
gl_entries.append(
self.get_gl_dict({
"account": income_or_expense_account,
"against": supplier_or_customer,
rev_dr_cr: flt(discount_amount, item.precision('discount_amount')),
rev_dr_cr + "_in_account_currency": flt(discount_amount * self.get('conversion_rate'),
item.precision('discount_amount')),
"cost_center": item.cost_center,
"project": item.project or self.project
}, account_currency, item=item)
)
if self.get('discount_amount') and self.get('additional_discount_account'):
gl_entries.append(
self.get_gl_dict({
"account": self.additional_discount_account,
"against": supplier_or_customer,
dr_or_cr: self.discount_amount,
"cost_center": self.cost_center
}, item=self)
)
def allocate_advance_taxes(self, gl_entries):
tax_map = self.get_tax_map()
for pe in self.get("advances"):
@@ -1096,6 +1179,8 @@ class AccountsController(TransactionBase):
if self.doctype in ("Sales Invoice", "Purchase Invoice"):
base_grand_total = base_grand_total - flt(self.base_write_off_amount)
grand_total = grand_total - flt(self.write_off_amount)
po_or_so, doctype, fieldname = self.get_order_details()
automatically_fetch_payment_terms = cint(frappe.db.get_single_value('Accounts Settings', 'automatically_fetch_payment_terms'))
if self.get("total_advance"):
if party_account_currency == self.company_currency:
@@ -1106,22 +1191,86 @@ class AccountsController(TransactionBase):
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
if not self.get("payment_schedule"):
if self.get("payment_terms_template"):
if self.doctype in ["Sales Invoice", "Purchase Invoice"] and automatically_fetch_payment_terms \
and self.linked_order_has_payment_terms(po_or_so, fieldname, doctype):
self.fetch_payment_terms_from_order(po_or_so, doctype)
if self.get('payment_terms_template'):
self.ignore_default_payment_terms_template = 1
elif self.get("payment_terms_template"):
data = get_payment_terms(self.payment_terms_template, posting_date, grand_total, base_grand_total)
for item in data:
self.append("payment_schedule", item)
else:
elif self.doctype not in ["Purchase Receipt"]:
data = dict(due_date=due_date, invoice_portion=100, payment_amount=grand_total, base_payment_amount=base_grand_total)
self.append("payment_schedule", data)
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
for d in self.get("payment_schedule"):
if d.invoice_portion:
d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
d.base_payment_amount = flt(base_grand_total * flt(d.invoice_portion / 100), d.precision('base_payment_amount'))
d.outstanding = d.payment_amount
elif not d.invoice_portion:
d.base_payment_amount = flt(base_grand_total * self.get("conversion_rate"), d.precision('base_payment_amount'))
def get_order_details(self):
if self.doctype == "Sales Invoice":
po_or_so = self.get('items')[0].get('sales_order')
po_or_so_doctype = "Sales Order"
po_or_so_doctype_name = "sales_order"
else:
po_or_so = self.get('items')[0].get('purchase_order')
po_or_so_doctype = "Purchase Order"
po_or_so_doctype_name = "purchase_order"
return po_or_so, po_or_so_doctype, po_or_so_doctype_name
def linked_order_has_payment_terms(self, po_or_so, fieldname, doctype):
if po_or_so and self.all_items_have_same_po_or_so(po_or_so, fieldname):
if self.linked_order_has_payment_terms_template(po_or_so, doctype):
return True
elif self.linked_order_has_payment_schedule(po_or_so):
return True
return False
def all_items_have_same_po_or_so(self, po_or_so, fieldname):
for item in self.get('items'):
if item.get(fieldname) != po_or_so:
return False
return True
def linked_order_has_payment_terms_template(self, po_or_so, doctype):
return frappe.get_value(doctype, po_or_so, 'payment_terms_template')
def linked_order_has_payment_schedule(self, po_or_so):
return frappe.get_all('Payment Schedule', filters={'parent': po_or_so})
def fetch_payment_terms_from_order(self, po_or_so, po_or_so_doctype):
"""
Fetch Payment Terms from Purchase/Sales Order on creating a new Purchase/Sales Invoice.
"""
po_or_so = frappe.get_cached_doc(po_or_so_doctype, po_or_so)
self.payment_schedule = []
self.payment_terms_template = po_or_so.payment_terms_template
for schedule in po_or_so.payment_schedule:
payment_schedule = {
'payment_term': schedule.payment_term,
'due_date': schedule.due_date,
'invoice_portion': schedule.invoice_portion,
'mode_of_payment': schedule.mode_of_payment,
'description': schedule.description
}
if schedule.discount_type == 'Percentage':
payment_schedule['discount_type'] = schedule.discount_type
payment_schedule['discount'] = schedule.discount
self.append("payment_schedule", payment_schedule)
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -1288,6 +1437,27 @@ def validate_taxes_and_charges(tax):
tax.rate = None
def validate_account_head(tax, doc):
company = frappe.get_cached_value('Account',
tax.account_head, 'company')
if company != doc.company:
frappe.throw(_('Row {0}: Account {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.account_head), frappe.bold(doc.company)), title=_('Invalid Account'))
def validate_cost_center(tax, doc):
if not tax.cost_center:
return
company = frappe.get_cached_value('Cost Center',
tax.cost_center, 'company')
if company != doc.company:
frappe.throw(_('Row {0}: Cost Center {1} does not belong to Company {2}')
.format(tax.idx, frappe.bold(tax.cost_center), frappe.bold(doc.company)), title=_('Invalid Cost Center'))
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
@@ -1507,7 +1677,7 @@ def set_child_tax_template_and_map(item, child_item, parent_doc):
if child_item.get("item_tax_template"):
child_item.item_tax_rate = get_item_tax_map(parent_doc.get('company'), child_item.item_tax_template, as_json=True)
def add_taxes_from_tax_template(child_item, parent_doc):
def add_taxes_from_tax_template(child_item, parent_doc, db_insert=True):
add_taxes_from_item_tax_template = frappe.db.get_single_value("Accounts Settings", "add_taxes_from_item_tax_template")
if child_item.get("item_tax_rate") and add_taxes_from_item_tax_template:
@@ -1530,7 +1700,8 @@ def add_taxes_from_tax_template(child_item, parent_doc):
"category" : "Total",
"add_deduct_tax" : "Add"
})
tax_row.db_insert()
if db_insert:
tax_row.db_insert()
def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
"""
@@ -1807,4 +1978,4 @@ def validate_regional(doc):
@erpnext.allow_regional
def validate_einvoice_fields(doc):
pass
pass

View File

@@ -72,7 +72,8 @@ class BuyingController(StockController, Subcontracting):
# set contact and address details for supplier, if they are not mentioned
if getattr(self, "supplier", None):
self.update_if_missing(get_party_details(self.supplier, party_type="Supplier", ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address')))
doctype=self.doctype, company=self.company, party_address=self.supplier_address, shipping_address=self.get('shipping_address'),
fetch_payment_terms_template= not self.get('ignore_default_payment_terms_template')))
self.set_missing_item_details(for_validate)

View File

@@ -27,6 +27,7 @@ class StockController(AccountsController):
if not self.get('is_return'):
self.validate_inspection()
self.validate_serialized_batch()
self.clean_serial_nos()
self.validate_customer_provided_item()
self.set_rate_of_stock_uom()
self.validate_internal_transfer()
@@ -72,6 +73,12 @@ class StockController(AccountsController):
frappe.throw(_("Row #{0}: The batch {1} has already expired.")
.format(d.idx, get_link_to_form("Batch", d.get("batch_no"))))
def clean_serial_nos(self):
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
# replace commas by linefeed and remove all spaces in string
row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "")
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):

View File

@@ -679,17 +679,13 @@ class calculate_taxes_and_totals(object):
default_mode_of_payment = frappe.db.get_value('POS Payment Method',
{'parent': self.doc.pos_profile, 'default': 1}, ['mode_of_payment'], as_dict=1)
self.doc.payments = []
if default_mode_of_payment:
self.doc.payments = []
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
'amount': total_amount_to_pay,
'default': 1
})
else:
self.doc.is_pos = 0
self.doc.pos_profile = ''
self.calculate_paid_amount()

View File

@@ -53,6 +53,13 @@ frappe.ui.form.on("Opportunity", {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
},
status:function(frm){
if (frm.doc.status == "Lost"){
frm.trigger('set_as_lost_dialog');
}
},
customer_address: function(frm, cdt, cdn) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display', false);
},
@@ -91,11 +98,6 @@ frappe.ui.form.on("Opportunity", {
frm.add_custom_button(__('Quotation'),
cur_frm.cscript.create_quotation, __('Create'));
if(doc.status!=="Quotation") {
frm.add_custom_button(__('Lost'), () => {
frm.trigger('set_as_lost_dialog');
});
}
}
if(!frm.doc.__islocal && frm.perm[0].write && frm.doc.docstatus==0) {

View File

@@ -430,7 +430,8 @@ regional_overrides = {
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount'
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount',
'erpnext.stock.doctype.item.item.set_item_tax_from_hsn_code': 'erpnext.regional.india.utils.set_item_tax_from_hsn_code'
},
'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',

View File

@@ -135,7 +135,7 @@ def mark_attendance(employee, attendance_date, status, shift=None, leave_type=No
def mark_bulk_attendance(data):
import json
from pprint import pprint
if isinstance(data, frappe.string_types):
if isinstance(data, str):
data = json.loads(data)
data = frappe._dict(data)
company = frappe.get_value('Employee', data.employee, 'company')

View File

@@ -0,0 +1,21 @@
frappe.pages['organizational-chart'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
parent: wrapper,
title: __('Organizational Chart'),
single_column: true
});
$(wrapper).bind('show', () => {
frappe.require('/assets/js/hierarchy-chart.min.js', () => {
let organizational_chart = undefined;
let method = 'erpnext.hr.page.organizational_chart.organizational_chart.get_children';
if (frappe.is_mobile()) {
organizational_chart = new erpnext.HierarchyChartMobile('Employee', wrapper, method);
} else {
organizational_chart = new erpnext.HierarchyChart('Employee', wrapper, method);
}
organizational_chart.show();
});
});
};

View File

@@ -0,0 +1,26 @@
{
"content": null,
"creation": "2021-05-25 10:53:10.107241",
"docstatus": 0,
"doctype": "Page",
"idx": 0,
"modified": "2021-05-25 10:53:18.201931",
"modified_by": "Administrator",
"module": "HR",
"name": "organizational-chart",
"owner": "Administrator",
"page_name": "Organizational Chart",
"roles": [
{
"role": "HR User"
},
{
"role": "HR Manager"
}
],
"script": null,
"standard": "Yes",
"style": null,
"system_page": 0,
"title": "Organizational Chart"
}

View File

@@ -0,0 +1,47 @@
from __future__ import unicode_literals
import frappe
@frappe.whitelist()
def get_children(parent=None, company=None, exclude_node=None):
filters = [['status', '!=', 'Left']]
if company and company != 'All Companies':
filters.append(['company', '=', company])
if parent and company and parent != company:
filters.append(['reports_to', '=', parent])
else:
filters.append(['reports_to', '=', ''])
if exclude_node:
filters.append(['name', '!=', exclude_node])
employees = frappe.get_list('Employee',
fields=['employee_name as name', 'name as id', 'reports_to', 'image', 'designation as title'],
filters=filters,
order_by='name'
)
for employee in employees:
is_expandable = frappe.db.count('Employee', filters={'reports_to': employee.get('id')})
employee.connections = get_connections(employee.id)
employee.expandable = 1 if is_expandable else 0
return employees
def get_connections(employee):
num_connections = 0
connections = frappe.get_list('Employee', filters=[
['reports_to', '=', employee]
])
num_connections += len(connections)
while connections:
for entry in connections:
connections = frappe.get_list('Employee', filters=[
['reports_to', '=', entry.name]
])
num_connections += len(connections)
return num_connections

View File

@@ -717,9 +717,8 @@ def get_bom_item_rate(args, bom_doc):
"ignore_conversion_rate": True
})
item_doc = frappe.get_cached_doc("Item", args.get("item_code"))
out = frappe._dict()
get_price_list_rate(bom_args, item_doc, out)
rate = out.price_list_rate
price_list_data = get_price_list_rate(bom_args, item_doc)
rate = price_list_data.price_list_rate
return rate
@@ -774,7 +773,7 @@ def get_bom_items_as_dict(bom, company, qty=1, fetch_exploded=1, fetch_scrap_ite
item.image,
bom.project,
bom_item.rate,
bom_item.amount,
sum(bom_item.{qty_field}/ifnull(bom.quantity, 1)) * bom_item.rate * %(qty)s as amount,
item.stock_uom,
item.item_group,
item.allow_alternative_item,

View File

@@ -608,6 +608,11 @@ def make_stock_entry(source_name, target_doc=None):
target.set_missing_values()
target.set_stock_entry_type()
wo_allows_alternate_item = frappe.db.get_value("Work Order", target.work_order, "allow_alternative_item")
for item in target.items:
item.allow_alternative_item = int(wo_allows_alternate_item and
frappe.get_cached_value("Item", item.item_code, "allow_alternative_item"))
doclist = get_mapped_doc("Job Card", source_name, {
"Job Card": {
"doctype": "Stock Entry",
@@ -698,4 +703,4 @@ def make_corrective_job_card(source_name, operation=None, for_operation=None, ta
}
}, target_doc, set_missing_values)
return doclist
return doclist

View File

@@ -109,6 +109,15 @@ class ProductionPlan(Document):
so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)]
return so_mr_list
def get_bom_item(self):
"""Check if Item or if its Template has a BOM."""
bom_item = None
has_bom = frappe.db.exists({'doctype': 'BOM', 'item': self.item_code, 'docstatus': 1})
if not has_bom:
template_item = frappe.db.get_value('Item', self.item_code, ['variant_of'])
bom_item = "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item
return bom_item
def get_so_items(self):
# Check for empty table or empty rows
if not self.get("sales_orders") or not self.get_so_mr_list("sales_order", "sales_orders"):
@@ -117,16 +126,26 @@ class ProductionPlan(Document):
so_list = self.get_so_mr_list("sales_order", "sales_orders")
item_condition = ""
if self.item_code:
bom_item = "bom.item = so_item.item_code"
if self.item_code and frappe.db.exists('Item', self.item_code):
bom_item = self.get_bom_item() or bom_item
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
items = frappe.db.sql("""select distinct parent, item_code, warehouse,
(qty - work_order_qty) * conversion_factor as pending_qty, description, name
from `tabSales Order Item` so_item
where parent in (%s) and docstatus = 1 and qty > work_order_qty
and exists (select name from `tabBOM` bom where bom.item=so_item.item_code
and bom.is_active = 1) %s""" % \
(", ".join(["%s"] * len(so_list)), item_condition), tuple(so_list), as_dict=1)
items = frappe.db.sql("""
select
distinct parent, item_code, warehouse,
(qty - work_order_qty) * conversion_factor as pending_qty,
description, name
from
`tabSales Order Item` so_item
where
parent in (%s) and docstatus = 1 and qty > work_order_qty
and exists (select name from `tabBOM` bom where %s
and bom.is_active = 1) %s""" %
(", ".join(["%s"] * len(so_list)),
bom_item,
item_condition),
tuple(so_list), as_dict=1)
if self.item_code:
item_condition = ' and so_item.item_code = {0}'.format(frappe.db.escape(self.item_code))
@@ -683,6 +702,7 @@ def get_material_request_items(row, sales_order, company,
def get_sales_orders(self):
so_filter = item_filter = ""
bom_item = "bom.item = so_item.item_code"
if self.from_date:
so_filter += " and so.transaction_date >= %(from_date)s"
if self.to_date:
@@ -694,7 +714,8 @@ def get_sales_orders(self):
if self.sales_order_status:
so_filter += "and so.status = %(sales_order_status)s"
if self.item_code:
if self.item_code and frappe.db.exists('Item', self.item_code):
bom_item = self.get_bom_item() or bom_item
item_filter += " and so_item.item_code = %(item)s"
open_so = frappe.db.sql("""
@@ -704,13 +725,13 @@ def get_sales_orders(self):
and so.docstatus = 1 and so.status not in ("Stopped", "Closed")
and so.company = %(company)s
and so_item.qty > so_item.work_order_qty {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=so_item.item_code
and (exists (select name from `tabBOM` bom where {2}
and bom.is_active = 1)
or exists (select name from `tabPacked Item` pi
where pi.parent = so.name and pi.parent_item = so_item.item_code
and exists (select name from `tabBOM` bom where bom.item=pi.item_code
and bom.is_active = 1)))
""".format(so_filter, item_filter), {
""".format(so_filter, item_filter, bom_item), {
"from_date": self.from_date,
"to_date": self.to_date,
"customer": self.customer,

View File

@@ -11,6 +11,7 @@ from erpnext.manufacturing.doctype.production_plan.production_plan import get_sa
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import create_stock_reconciliation
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.manufacturing.doctype.production_plan.production_plan import get_items_for_material_requests, get_warehouse_list
from erpnext.controllers.item_variant import create_variant
class TestProductionPlan(unittest.TestCase):
def setUp(self):
@@ -271,6 +272,60 @@ class TestProductionPlan(unittest.TestCase):
self.assertEqual(warehouses, expected_warehouses)
def test_get_sales_order_with_variant(self):
if not frappe.db.exists('Item', {"item_code": 'PIV'}):
item = create_item('PIV', valuation_rate = 100)
variant_settings = {
"attributes": [
{
"attribute": "Colour"
},
],
"has_variants": 1
}
item.update(variant_settings)
item.save()
parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
if not frappe.db.exists('BOM', {"item": 'PIV'}):
parent_bom = make_bom(item = 'PIV', raw_materials = ['PIV'])
else:
parent_bom = frappe.get_doc('BOM', {"item": 'PIV'})
if not frappe.db.exists('Item', {"item_code": 'PIV-RED'}):
variant = create_variant("PIV", {"Colour": "Red"})
variant.save()
variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
else:
variant = frappe.get_doc('Item', 'PIV-RED')
if not frappe.db.exists('BOM', {"item": 'PIV-RED'}):
variant_bom = make_bom(item = variant.item_code, raw_materials = [variant.item_code])
"""Testing when item variant has a BOM"""
so = make_sales_order(item_code="PIV-RED", qty=5)
pln = frappe.new_doc('Production Plan')
pln.company = so.company
pln.get_items_from = 'Sales Order'
pln.item_code = 'PIV-RED'
pln.get_open_sales_orders()
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
pln.get_so_items()
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
self.assertEqual(pln.po_items[0].bom_no, variant_bom.name)
so.cancel()
frappe.delete_doc('Sales Order', so.name)
variant_bom.cancel()
frappe.delete_doc('BOM', variant_bom.name)
"""Testing when item variant doesn't have a BOM"""
so = make_sales_order(item_code="PIV-RED", qty=5)
pln.get_open_sales_orders()
self.assertEqual(pln.sales_orders[0].sales_order, so.name)
pln.po_items = []
pln.get_so_items()
self.assertEqual(pln.po_items[0].item_code, 'PIV-RED')
self.assertEqual(pln.po_items[0].bom_no, parent_bom.name)
frappe.db.rollback()
def create_production_plan(**args):
args = frappe._dict(args)

View File

@@ -655,7 +655,7 @@ class WorkOrder(Document):
for item in sorted(item_dict.values(), key=lambda d: d['idx'] or 9999):
self.append('required_items', {
'rate': item.rate,
'amount': item.amount,
'amount': item.rate * item.qty,
'operation': item.operation or operation,
'item_code': item.item_code,
'item_name': item.item_name,

View File

@@ -293,6 +293,9 @@ erpnext.patches.v13_0.update_job_card_details
erpnext.patches.v13_0.update_level_in_bom #1234sswef
erpnext.patches.v13_0.add_missing_fg_item_for_stock_entry
erpnext.patches.v13_0.update_subscription_status_in_memberships
erpnext.patches.v13_0.update_amt_in_work_order_required_items
erpnext.patches.v12_0.show_einvoice_irn_cancelled_field
erpnext.patches.v13_0.delete_orphaned_tables
erpnext.patches.v13_0.update_export_type_for_gst
erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.update_recipient_email_digest

View File

@@ -0,0 +1,12 @@
from __future__ import unicode_literals
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
irn_cancelled_field = frappe.db.exists('Custom Field', {'dt': 'Sales Invoice', 'fieldname': 'irn_cancelled'})
if irn_cancelled_field:
frappe.db.set_value('Custom Field', irn_cancelled_field, 'depends_on', 'eval: doc.irn')
frappe.db.set_value('Custom Field', irn_cancelled_field, 'read_only', 0)

View File

@@ -30,19 +30,20 @@ def execute():
return
repost_stock_entries = []
stock_entries = frappe.db.sql_list('''
SELECT
se.name
FROM
`tabStock Entry` se
WHERE
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in {work_orders}
se.purpose = 'Manufacture' and se.docstatus < 2 and se.work_order in %s
and not exists(
select name from `tabStock Entry Detail` sed where sed.parent = se.name and sed.is_finished_item = 1
)
Order BY
ORDER BY
se.posting_date, se.posting_time
'''.format(work_orders=tuple(work_orders)))
''', (work_orders,))
if stock_entries:
print('Length of stock entries', len(stock_entries))

View File

@@ -0,0 +1,69 @@
# Copyright (c) 2019, Frappe and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import getdate
def execute():
frappe.reload_doc('setup', 'doctype', 'transaction_deletion_record')
if has_deleted_company_transactions():
child_doctypes = get_child_doctypes_whose_parent_doctypes_were_affected()
for doctype in child_doctypes:
docs = frappe.get_all(doctype, fields=['name', 'parent', 'parenttype', 'creation'])
for doc in docs:
if not frappe.db.exists(doc['parenttype'], doc['parent']):
frappe.db.delete(doctype, {'name': doc['name']})
elif check_for_new_doc_with_same_name_as_deleted_parent(doc):
frappe.db.delete(doctype, {'name': doc['name']})
def has_deleted_company_transactions():
return frappe.get_all('Transaction Deletion Record')
def get_child_doctypes_whose_parent_doctypes_were_affected():
parent_doctypes = get_affected_doctypes()
child_doctypes = frappe.get_all(
'DocField',
filters={
'fieldtype': 'Table',
'parent':['in', parent_doctypes]
}, pluck='options')
return child_doctypes
def get_affected_doctypes():
affected_doctypes = []
tdr_docs = frappe.get_all('Transaction Deletion Record', pluck="name")
for tdr in tdr_docs:
tdr_doc = frappe.get_doc("Transaction Deletion Record", tdr)
for doctype in tdr_doc.doctypes:
if is_not_child_table(doctype.doctype_name):
affected_doctypes.append(doctype.doctype_name)
affected_doctypes = remove_duplicate_items(affected_doctypes)
return affected_doctypes
def is_not_child_table(doctype):
return not bool(frappe.get_value('DocType', doctype, 'istable'))
def remove_duplicate_items(affected_doctypes):
return list(set(affected_doctypes))
def check_for_new_doc_with_same_name_as_deleted_parent(doc):
"""
Compares creation times of parent and child docs.
Since Transaction Deletion Record resets the naming series after deletion,
it allows the creation of new docs with the same names as the deleted ones.
"""
parent_creation_time = frappe.db.get_value(doc['parenttype'], doc['parent'], 'creation')
child_creation_time = doc['creation']
return getdate(parent_creation_time) > getdate(child_creation_time)

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
""" Correct amount in child table of required items table."""
frappe.reload_doc("manufacturing", "doctype", "work_order")
frappe.reload_doc("manufacturing", "doctype", "work_order_item")
frappe.db.sql("""UPDATE `tabWork Order Item` SET amount = rate * required_qty""")

View File

@@ -112,11 +112,11 @@ class AdditionalSalary(Document):
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days
@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type):
additional_salary_list = frappe.db.sql("""
select name, salary_component as component, type, amount, overwrite_salary_structure_amount as overwrite,
deduct_full_tax_on_selected_payroll_date, is_recurring
select name, salary_component as component, type, amount,
overwrite_salary_structure_amount as overwrite,
deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary`
where employee=%(employee)s
and docstatus = 1

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