Merge branch 'develop' into crm-carry-forward-communication-comments

This commit is contained in:
Anupam Kumar 2021-12-14 16:42:59 +05:30 committed by GitHub
commit ea5e155823
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
151 changed files with 4840 additions and 1887 deletions

View File

@ -1,47 +0,0 @@
---
name: Bug report
about: Report a bug encountered while using ERPNext
labels: bug
---
<!--
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the manual https://erpnext.com/docs/user/manual/en or use https://discuss.erpnext.com
- For documentation issues, refer to https://github.com/frappe/erpnext_com
2. Use the search function before creating a new issue. Duplicates will be closed and directed to
the original discussion.
3. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
4. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
-->
## Description of the issue
## Context information (for bug reports)
**Output of `bench version`**
```
(paste here)
```
## Steps to reproduce the issue
1.
2.
3.
### Observed result
### Expected result
### Stacktrace / full error message
```
(paste here)
```
## Additional information
OS version / distribution, `ERPNext` install method, etc.

106
.github/ISSUE_TEMPLATE/bug_report.yaml vendored Normal file
View File

@ -0,0 +1,106 @@
name: Bug Report
description: Report a bug encountered while using ERPNext
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
Welcome to ERPNext issue tracker! Before creating an issue, please heed the following:
1. This tracker should only be used to report bugs and request features / enhancements to ERPNext
- For questions and general support, checkout the [user manual](https://docs.erpnext.com/) or use [forum](https://discuss.erpnext.com)
- For documentation issues, propose edit on [documentation site](https://docs.erpnext.com/) directly.
2. When making a bug report, make sure you provide all required information. The easier it is for
maintainers to reproduce, the faster it'll be fixed.
3. If you think you know what the reason for the bug is, share it with us. Maybe put in a PR 😉
- type: textarea
id: bug-info
attributes:
label: Information about bug
description: Also tell us, what did you expect to happen?
placeholder: Please provide as much information as possible.
validations:
required: true
- type: dropdown
id: version
attributes:
label: Version
description: Affected versions.
multiple: true
options:
- v12
- v13
- v14
- develop
validations:
required: true
- type: dropdown
id: module
attributes:
label: Module
description: Select affected module of ERPNext.
multiple: true
options:
- accounts
- stock
- buying
- selling
- ecommerce
- manufacturing
- HR
- projects
- support
- assets
- integrations
- quality
- regional
- portal
- agriculture
- education
- non-profit
validations:
required: true
- type: textarea
id: exact-version
attributes:
label: Version
description: Share exact version number of Frappe and ERPNext you are using.
placeholder: |
Frappe version -
ERPNext Verion -
validations:
required: true
- type: dropdown
id: install-method
attributes:
label: Installation method
options:
- docker
- easy-install
- manual install
- FrappeCloud
validations:
required: true
- type: textarea
id: logs
attributes:
label: Relevant log output / Stack trace / Full Error Message.
description: Please copy and paste any relevant log output. This will be automatically formatted.
render: shell
- type: checkboxes
id: terms
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/frappe/erpnext/blob/develop/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

View File

@ -1,7 +1,10 @@
---
name: Feature request
about: Suggest an idea to improve ERPNext
title: ''
labels: feature-request
assignees: ''
---
<!--

View File

@ -1,17 +0,0 @@
---
name: Question about using ERPNext
about: This is not the appropriate channel
labels: invalid
---
Please post on our forums:
for questions about using `ERPNext`: https://discuss.erpnext.com
for questions about using the `Frappe Framework`: ~~https://discuss.frappe.io~~ => [stackoverflow](https://stackoverflow.com/questions/tagged/frappe) tagged under `frappe`
for questions about using `bench`, probably the best place to start is the [bench repo](https://github.com/frappe/bench)
For documentation issues, use the [ERPNext Documentation](https://erpnext.com/docs/) or [Frappe Framework Documentation](https://frappe.io/docs/user/en) or the [developer cheetsheet](https://github.com/frappe/frappe/wiki/Developer-Cheatsheet)
> **Posts that are not bug reports or feature requests will not be addressed on this issue tracker.**

56
.github/stale.yml vendored
View File

@ -1,34 +1,36 @@
# Configuration for probot-stale - https://github.com/probot/stale
# Number of days of inactivity before an Issue or Pull Request becomes stale
daysUntilStale: 15
# Number of days of inactivity before a stale Issue or Pull Request is closed.
# Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale.
daysUntilClose: 3
# Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable
exemptLabels:
- hotfix
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: false
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
# Label to use when marking as stale
staleLabel: inactive
# Comment to post when marking as stale. Set to `false` to disable
markComment: >
This pull request has been automatically marked as stale because it has not had
recent activity. It will be closed within a week if no further activity occurs, but it
only takes a comment to keep a contribution alive :) Also, even if it is closed,
you can always reopen the PR when you're ready. Thank you for contributing.
# Limit the number of actions per hour, from 1-30. Default is 30
limitPerRun: 30
limitPerRun: 10
# Limit to only `issues` or `pulls`
only: pulls
# Set to true to ignore issues in a project (defaults to false)
exemptProjects: true
# Set to true to ignore issues in a milestone (defaults to false)
exemptMilestones: true
pulls:
daysUntilStale: 15
daysUntilClose: 3
exemptLabels:
- hotfix
markComment: >
This pull request has been automatically marked as inactive because it has
not had recent activity. It will be closed within 3 days if no further
activity occurs, but it only takes a comment to keep a contribution alive
:) Also, even if it is closed, you can always reopen the PR when you're
ready. Thank you for contributing.
issues:
daysUntilStale: 60
daysUntilClose: 7
exemptLabels:
- valid
- to-validate
markComment: >
This issue has been automatically marked as inactive because it has not had
recent activity and it wasn't validated by maintainer team. It will be
closed within a week if no further activity occurs.

View File

@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.db.commit()
except Exception as e:
if frappe.flags.in_test:
traceback = frappe.get_traceback()
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(message=traceback)
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
if submit:
journal_entry.submit()
frappe.db.commit()
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(message=traceback)
frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
frappe.flags.deferred_accounting_error = True

View File

@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
from frappe.model.document import Document
from frappe.utils import cint
from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document):
def on_update(self):
@ -25,6 +27,7 @@ class AccountsSettings(Document):
self.validate_stale_days()
self.enable_payment_schedule_in_print()
self.toggle_discount_accounting_fields()
self.validate_pending_reposts()
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
@ -56,3 +59,8 @@ class AccountsSettings(Document):
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)
def validate_pending_reposts(self):
if self.acc_frozen_upto:
check_pending_reposting(self.acc_frozen_upto)

View File

@ -5,10 +5,10 @@
import unittest
import frappe
from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
test_records = frappe.get_test_records('Fiscal Year')
test_ignore = ["Company"]
class TestFiscalYear(unittest.TestCase):
@ -25,3 +25,29 @@ class TestFiscalYear(unittest.TestCase):
})
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
def test_record_generator():
test_records = [
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
}
]
start = 2012
end = now_datetime().year + 5
for year in range(start, end):
test_records.append({
"doctype": "Fiscal Year",
"year": f"_Test Fiscal Year {year}",
"year_start_date": f"{year}-01-01",
"year_end_date": f"{year}-12-31"
})
return test_records
test_records = test_record_generator()

View File

@ -1,69 +0,0 @@
[
{
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2012",
"year_end_date": "2012-12-31",
"year_start_date": "2012-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2013",
"year_end_date": "2013-12-31",
"year_start_date": "2013-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2014",
"year_end_date": "2014-12-31",
"year_start_date": "2014-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2015",
"year_end_date": "2015-12-31",
"year_start_date": "2015-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2016",
"year_end_date": "2016-12-31",
"year_start_date": "2016-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2017",
"year_end_date": "2017-12-31",
"year_start_date": "2017-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2018",
"year_end_date": "2018-12-31",
"year_start_date": "2018-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2019",
"year_end_date": "2019-12-31",
"year_start_date": "2019-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2020",
"year_end_date": "2020-12-31",
"year_start_date": "2020-01-01"
},
{
"doctype": "Fiscal Year",
"year": "_Test Fiscal Year 2021",
"year_end_date": "2021-12-31",
"year_start_date": "2021-01-01"
}
]

View File

@ -171,6 +171,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
"amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@ -1561,16 +1562,23 @@
"label": "Coupon Code",
"options": "Coupon Code",
"print_hide": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
}
],
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2021-08-27 20:12:57.306772",
"modified": "2021-10-05 12:11:53.871828",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -46,6 +46,7 @@
"base_amount",
"pricing_rules",
"is_free_item",
"grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@ -800,14 +801,22 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-01-04 17:34:49.924531",
"modified": "2021-10-05 12:23:47.506290",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@ -3,7 +3,8 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
frappe.ui.form.on("POS Profile", "onload", function(frm) {
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("selling_price_list", function() {
return { filters: { selling: 1 } };
});
@ -15,10 +16,7 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) {
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
});
frappe.ui.form.on('POS Profile', {
setup: function(frm) {
frm.set_query("print_format", function() {
return {
filters: [
@ -27,10 +25,16 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query("account_for_change_amount", function() {
frm.set_query("account_for_change_amount", function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
account_type: ['in', ["Cash", "Bank"]]
account_type: ['in', ["Cash", "Bank"]],
is_group: 0,
company: doc.company
}
};
});
@ -45,7 +49,7 @@ frappe.ui.form.on('POS Profile', {
});
frm.set_query('company_address', function(doc) {
if(!doc.company) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
@ -58,11 +62,79 @@ frappe.ui.form.on('POS Profile', {
};
});
frm.set_query('income_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
});
frm.set_query('cost_center', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
'company': doc.company,
'is_group': 0
}
};
});
frm.set_query('expense_account', function(doc) {
if (!doc.company) {
frappe.throw(__('Please set Company'));
}
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
});
frm.set_query("select_print_heading", function() {
return {
filters: [
['Print Heading', 'docstatus', '!=', 2]
]
};
});
frm.set_query("write_off_account", function(doc) {
return {
filters: {
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
});
frm.set_query("write_off_cost_center", function(doc) {
return {
filters: {
'is_group': 0,
'company': doc.company
}
};
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
if(frm.doc.company) {
if (frm.doc.company) {
frm.trigger("toggle_display_account_head");
}
},
@ -76,71 +148,4 @@ frappe.ui.form.on('POS Profile', {
frm.toggle_display('expense_account',
erpnext.is_perpetual_inventory_enabled(frm.doc.company));
}
})
// Income Account
// --------------------------------
cur_frm.fields_dict['income_account'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'is_group': 0,
'company': doc.company,
'account_type': "Income Account"
}
};
};
// Cost Center
// -----------------------------
cur_frm.fields_dict['cost_center'].get_query = function(doc,cdt,cdn) {
return{
filters:{
'company': doc.company,
'is_group': 0
}
};
};
// Expense Account
// -----------------------------
cur_frm.fields_dict["expense_account"].get_query = function(doc) {
return {
filters: {
"report_type": "Profit and Loss",
"company": doc.company,
"is_group": 0
}
};
};
// ------------------ Get Print Heading ------------------------------------
cur_frm.fields_dict['select_print_heading'].get_query = function(doc, cdt, cdn) {
return{
filters:[
['Print Heading', 'docstatus', '!=', 2]
]
};
};
cur_frm.fields_dict.write_off_account.get_query = function(doc) {
return{
filters:{
'report_type': 'Profit and Loss',
'is_group': 0,
'company': doc.company
}
};
};
// Write off cost center
// -----------------------
cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
return{
filters:{
'is_group': 0,
'company': doc.company
}
};
};
});

View File

@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController):
self.set_status()
self.validate_purchase_receipt_if_update_stock()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse")
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
def validate_release_date(self):
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
@ -294,8 +297,15 @@ class PurchaseInvoice(BuyingController):
item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code,
company = self.company)
if not asset_category_account:
form_link = get_link_to_form('Asset Category', asset_category)
throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account")
)
item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate:

View File

@ -516,15 +516,6 @@ cur_frm.fields_dict.write_off_cost_center.get_query = function(doc) {
}
}
// project name
//--------------------------
cur_frm.fields_dict['project'].get_query = function(doc, cdt, cdn) {
return{
query: "erpnext.controllers.queries.get_project_name",
filters: {'customer': doc.customer}
}
}
// Income Account in Details Table
// --------------------------------
cur_frm.set_query("income_account", "items", function(doc) {

View File

@ -182,6 +182,7 @@
"sales_team_section_break",
"sales_partner",
"column_break10",
"amount_eligible_for_commission",
"commission_rate",
"total_commission",
"section_break2",
@ -2019,6 +2020,12 @@
"label": "Total Billing Hours",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "amount_eligible_for_commission",
"fieldtype": "Currency",
"label": "Amount Eligible for Commission",
"read_only": 1
}
],
"icon": "fa fa-file-text",
@ -2031,7 +2038,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-10-11 20:19:38.667508",
"modified": "2021-10-21 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -155,6 +155,8 @@ class SalesInvoice(SellingController):
if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated:
validate_loyalty_points(self, self.loyalty_points)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_fixed_asset(self):
for d in self.get("items"):
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:

View File

@ -2385,6 +2385,29 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_sales_commission(self):
si = frappe.copy_doc(test_records[0])
item = copy.deepcopy(si.get('items')[0])
item.update({
"qty": 1,
"rate": 500,
"grant_commission": 1
})
si.append("items", item)
# Test valid values
for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)):
si.commission_rate = commission_rate
si.save()
self.assertEqual(si.amount_eligible_for_commission, 500)
self.assertEqual(si.total_commission, total_commission)
# Test invalid values
for commission_rate in (101, -1):
si.reload()
si.commission_rate = commission_rate
self.assertRaises(frappe.ValidationError, si.save)
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)

View File

@ -47,6 +47,7 @@
"pricing_rules",
"stock_uom_rate",
"is_free_item",
"grant_commission",
"section_break_21",
"net_rate",
"net_amount",
@ -828,15 +829,23 @@
"fieldtype": "Link",
"label": "Discount Account",
"options": "Account"
},
{
"default": "0",
"fieldname": "grant_commission",
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-08-19 13:41:53.435827",
"modified": "2021-10-05 12:24:54.968907",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@ -68,11 +68,13 @@ def _get_party_details(party=None, account=None, party_type="Customer", company=
party_details["tax_category"] = get_address_tax_category(party.get("tax_category"),
party_address, shipping_address if party_type != "Supplier" else party_address)
if not party_details.get("taxes_and_charges"):
party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company,
tax_template = set_taxes(party.name, party_type, posting_date, 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 tax_template:
party_details['taxes_and_charges'] = tax_template
if cint(fetch_payment_terms_template):
party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company)

View File

@ -109,7 +109,11 @@ class ReceivablePayableReport(object):
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
outstanding = 0.0
outstanding = 0.0,
invoiced_in_account_currency = 0.0,
paid_in_account_currency = 0.0,
credit_note_in_account_currency = 0.0,
outstanding_in_account_currency = 0.0
)
self.get_invoices(gle)
@ -150,21 +154,28 @@ class ReceivablePayableReport(object):
# gle_balance will be the total "debit - credit" for receivable type reports and
# and vice-versa for payable type reports
gle_balance = self.get_gle_balance(gle)
gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle)
if gle_balance > 0:
if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher:
# debit against sales / purchase invoice
row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
else:
# invoice
row.invoiced += gle_balance
row.invoiced_in_account_currency += gle_balance_in_account_currency
else:
# payment or credit note for receivables
if self.is_invoice(gle):
# stand alone debit / credit note
row.credit_note -= gle_balance
row.credit_note_in_account_currency -= gle_balance_in_account_currency
else:
# advance / unlinked payment or other adjustment
row.paid -= gle_balance
row.paid_in_account_currency -= gle_balance_in_account_currency
if gle.cost_center:
row.cost_center = str(gle.cost_center)
@ -216,8 +227,13 @@ class ReceivablePayableReport(object):
# as we can use this to filter out invoices without outstanding
for key, row in self.voucher_balance.items():
row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision)
row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \
row.credit_note_in_account_currency, self.currency_precision)
row.invoice_grand_total = row.invoiced
if abs(row.outstanding) > 1.0/10 ** self.currency_precision:
if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \
(abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision):
# non-zero oustanding, we must consider this row
if self.is_invoice(row) and self.filters.based_on_payment_terms:
@ -529,7 +545,9 @@ class ReceivablePayableReport(object):
def set_ageing(self, row):
if self.filters.ageing_based_on == "Due Date":
entry_date = row.due_date
# use posting date as a fallback for advances posted via journal and payment entry
# when ageing viewed by due date
entry_date = row.due_date or row.posting_date
elif self.filters.ageing_based_on == "Supplier Invoice Date":
entry_date = row.bill_date
else:
@ -583,12 +601,14 @@ class ReceivablePayableReport(object):
else:
select_fields = "debit, credit"
doc_currency_fields = "debit_in_account_currency, credit_in_account_currency"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0} {remarks}
against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks}
from
`tabGL Entry`
where
@ -596,8 +616,8 @@ class ReceivablePayableReport(object):
and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
{2} {3} {4}"""
.format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@ -718,6 +738,13 @@ class ReceivablePayableReport(object):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr) - self.get_reverse_balance(gle)
def get_gle_balance_in_account_currency(self, gle):
# get the balance of the GL (debit - credit) or reverse balance based on report type
return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle)
def get_reverse_balance_in_account_currency(self, gle):
return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency')
def get_reverse_balance(self, gle):
# get "credit" balance if report type is "debit" and vice versa
return gle.get('debit' if self.dr_or_cr=='credit' else 'credit')

View File

@ -92,6 +92,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Include Default Book Entries"),
"fieldtype": "Check",
"default": 1
},
{
"fieldname": "show_zero_values",
"label": __("Show zero values"),
"fieldtype": "Check"
}
],
"formatter": function(value, row, column, data, default_formatter) {

View File

@ -22,7 +22,11 @@ from erpnext.accounts.report.cash_flow.cash_flow import (
get_cash_flow_accounts,
)
from erpnext.accounts.report.cash_flow.cash_flow import get_report_summary as get_cash_flow_summary
from erpnext.accounts.report.financial_statements import get_fiscal_year_data, sort_accounts
from erpnext.accounts.report.financial_statements import (
filter_out_zero_value_rows,
get_fiscal_year_data,
sort_accounts,
)
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_chart_data as get_pl_chart_data,
)
@ -265,7 +269,7 @@ def get_columns(companies, filters):
return columns
def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False):
accounts, accounts_by_name = get_account_heads(root_type,
accounts, accounts_by_name, parent_children_map = get_account_heads(root_type,
companies, filters)
if not accounts: return []
@ -294,6 +298,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values"))
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@ -370,7 +376,7 @@ def get_account_heads(root_type, companies, filters):
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name
return accounts, accounts_by_name, parent_children_map
def update_parent_account_names(accounts):
"""Update parent_account_name in accounts list.

View File

@ -1,18 +1,21 @@
{
"add_total_row": 0,
"apply_user_permissions": 1,
"columns": [],
"creation": "2013-05-06 12:28:23",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 3,
"is_standard": "Yes",
"modified": "2017-03-06 05:52:57.645281",
"modified": "2021-10-06 06:26:07.881340",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Partners Commission",
"owner": "Administrator",
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:150\",\n\tsum(base_net_total) as \"Invoiced Amount (Exclusive Tax):Currency:210\",\n\tsum(total_commission) as \"Total Commission:Currency:150\",\n\tsum(total_commission)*100/sum(base_net_total) as \"Average Commission Rate:Currency:170\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
"prepared_report": 0,
"query": "SELECT\n sales_partner as \"Sales Partner:Link/Sales Partner:220\",\n\tsum(base_net_total) as \"Invoiced Amount (Excl. Tax):Currency:220\",\n\tsum(amount_eligible_for_commission) as \"Amount Eligible for Commission:Currency:220\",\n\tsum(total_commission) as \"Total Commission:Currency:170\",\n\tsum(total_commission)*100/sum(amount_eligible_for_commission) as \"Average Commission Rate:Percent:220\"\nFROM\n\t`tabSales Invoice`\nWHERE\n\tdocstatus = 1 and ifnull(base_net_total, 0) > 0 and ifnull(total_commission, 0) > 0\nGROUP BY\n\tsales_partner\nORDER BY\n\t\"Total Commission:Currency:120\"",
"ref_doctype": "Sales Invoice",
"report_name": "Sales Partners Commission",
"report_type": "Query Report",

View File

@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if not tax_withholding_category:
tax_withholding_category = supplier_map.get(supplier, {}).get('tax_withholding_category')
rate = tax_rate_map.get(tax_withholding_category)
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
total_amount_credited += (entry.credit - entry.debit)
if rate and tds_deducted:
if tds_deducted:
row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
'supplier': supplier_map.get(supplier, {}).get('name')
@ -67,7 +71,7 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
def get_supplier_pan_map():
supplier_map = frappe._dict()
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name', 'tax_withholding_category'])
for d in suppliers:
supplier_map[d.name] = d

View File

@ -80,20 +80,20 @@ frappe.ui.form.on('Asset', {
if (frm.doc.docstatus==1) {
if (in_list(["Submitted", "Partially Depreciated", "Fully Depreciated"], frm.doc.status)) {
frm.add_custom_button("Transfer Asset", function() {
frm.add_custom_button(__("Transfer Asset"), function() {
erpnext.asset.transfer_asset(frm);
}, __("Manage"));
frm.add_custom_button("Scrap Asset", function() {
frm.add_custom_button(__("Scrap Asset"), function() {
erpnext.asset.scrap_asset(frm);
}, __("Manage"));
frm.add_custom_button("Sell Asset", function() {
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() {
frm.add_custom_button(__("Restore Asset"), function() {
erpnext.asset.restore_asset(frm);
}, __("Manage"));
}
@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
if (frm.doc.status != 'Fully Depreciated') {
frm.add_custom_button(__("Adjust Asset Value"), function() {
frm.trigger("create_asset_adjustment");
frm.trigger("create_asset_value_adjustment");
}, __("Manage"));
}
@ -121,7 +121,7 @@ frappe.ui.form.on('Asset', {
}
if (frm.doc.purchase_receipt || !frm.doc.is_existing_asset) {
frm.add_custom_button("View 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,
@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
});
},
create_asset_adjustment: function(frm) {
create_asset_value_adjustment: function(frm) {
frappe.call({
args: {
"asset": frm.doc.name,
"asset_category": frm.doc.asset_category,
"company": frm.doc.company
},
method: "erpnext.assets.doctype.asset.asset.create_asset_adjustment",
method: "erpnext.assets.doctype.asset.asset.create_asset_value_adjustment",
freeze: 1,
callback: function(r) {
var doclist = frappe.model.sync(r.message);

View File

@ -185,84 +185,84 @@ class Asset(AccountsController):
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()
for finance_book in self.get('finance_books'):
self.validate_asset_finance_books(finance_book)
# value_after_depreciation - current Asset value
if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) -
flt(self.opening_accumulated_depreciation))
if self.docstatus == 1 and finance_book.value_after_depreciation:
value_after_depreciation = flt(finance_book.value_after_depreciation)
else:
value_after_depreciation = (flt(self.gross_purchase_amount) -
flt(self.opening_accumulated_depreciation))
d.value_after_depreciation = value_after_depreciation
finance_book.value_after_depreciation = value_after_depreciation
number_of_pending_depreciations = cint(d.total_number_of_depreciations) - \
number_of_pending_depreciations = cint(finance_book.total_number_of_depreciations) - \
cint(self.number_of_depreciations_booked)
has_pro_rata = self.check_is_pro_rata(d)
has_pro_rata = self.check_is_pro_rata(finance_book)
if has_pro_rata:
number_of_pending_depreciations += 1
skip_row = False
for n in range(start, number_of_pending_depreciations):
for n in range(start[finance_book.idx-1], number_of_pending_depreciations):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, finance_book)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(d.depreciation_start_date,
n * cint(d.frequency_of_depreciation))
schedule_date = add_months(finance_book.depreciation_start_date,
n * cint(finance_book.frequency_of_depreciation))
# schedule date will be a year later from start date
# so monthly schedule date is calculated by removing 11 months from it
monthly_schedule_date = add_months(schedule_date, - d.frequency_of_depreciation + 1)
monthly_schedule_date = add_months(schedule_date, - finance_book.frequency_of_depreciation + 1)
# if asset is being sold
if date_of_sale:
from_date = self.get_from_date(d.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
from_date = self.get_from_date(finance_book.finance_book)
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
from_date, date_of_sale)
if depreciation_amount > 0:
self.append("schedules", {
"schedule_date": date_of_sale,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
break
# For first row
if has_pro_rata and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date)
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
self.available_for_use_date, finance_book.depreciation_start_date)
# For first depr schedule date will be the start date
# so monthly schedule date is calculated by removing month difference between use date and start date
monthly_schedule_date = add_months(d.depreciation_start_date, - months + 1)
monthly_schedule_date = add_months(finance_book.depreciation_start_date, - months + 1)
# For last row
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
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))
(n + self.number_of_depreciations_booked) * cint(finance_book.frequency_of_depreciation))
depreciation_amount_without_pro_rata = depreciation_amount
depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book,
depreciation_amount, schedule_date, self.to_date)
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
depreciation_amount, d.finance_book)
depreciation_amount, finance_book.finance_book)
monthly_schedule_date = add_months(schedule_date, 1)
schedule_date = add_days(schedule_date, days)
@ -273,10 +273,10 @@ class Asset(AccountsController):
self.precision("gross_purchase_amount"))
# Adjust depreciation amount in the last period based on the expected value after useful life
if d.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != d.expected_value_after_useful_life)
or value_after_depreciation < d.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
and value_after_depreciation != finance_book.expected_value_after_useful_life)
or value_after_depreciation < finance_book.expected_value_after_useful_life):
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
skip_row = True
if depreciation_amount > 0:
@ -286,7 +286,7 @@ class Asset(AccountsController):
# In pro rata case, for first and last depreciation, month range would be different
month_range = months \
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
else d.frequency_of_depreciation
else finance_book.frequency_of_depreciation
for r in range(month_range):
if (has_pro_rata and n == 0):
@ -312,27 +312,52 @@ class Asset(AccountsController):
self.append("schedules", {
"schedule_date": date,
"depreciation_amount": amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
else:
self.append("schedules", {
"schedule_date": schedule_date,
"depreciation_amount": depreciation_amount,
"depreciation_method": d.depreciation_method,
"finance_book": d.finance_book,
"finance_book_id": d.idx
"depreciation_method": finance_book.depreciation_method,
"finance_book": finance_book.finance_book,
"finance_book_id": finance_book.idx
})
# used when depreciation schedule needs to be modified due to increase in asset life
# depreciation schedules need to be cleared before modification due to increase in asset life/asset sales
# JE: Journal Entry, FB: Finance Book
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
start = []
num_of_depreciations_completed = 0
depr_schedule = []
for schedule in self.get('schedules'):
# to update start when there are JEs linked with all the schedule rows corresponding to an FB
if len(start) == (int(schedule.finance_book_id) - 2):
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to ensure that start will only be updated once for each FB
if len(start) == (int(schedule.finance_book_id) - 1):
if schedule.journal_entry:
num_of_depreciations_completed += 1
depr_schedule.append(schedule)
else:
start.append(num_of_depreciations_completed)
num_of_depreciations_completed = 0
# to update start when all the schedule rows corresponding to the last FB are linked with JEs
if len(start) == (len(self.finance_books) - 1):
start.append(num_of_depreciations_completed)
# when the Depreciation Schedule is being created for the first time
if start == []:
start = [0] * len(self.finance_books)
else:
self.schedules = depr_schedule
return start
def get_from_date(self, finance_book):
@ -354,7 +379,12 @@ class Asset(AccountsController):
# 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 not existing asset, from_date = available_for_use_date
# otherwise, if number_of_depreciations_booked = 2, available_for_use_date = 01/01/2020 and frequency_of_depreciation = 12
# from_date = 01/01/2022
from_date = self.get_modified_available_for_use_date(row)
days = date_diff(row.depreciation_start_date, from_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)
@ -364,6 +394,9 @@ class Asset(AccountsController):
return has_pro_rata
def get_modified_available_for_use_date(self, row):
return add_months(self.available_for_use_date, (self.number_of_depreciations_booked * row.frequency_of_depreciation))
def validate_asset_finance_books(self, row):
if flt(row.expected_value_after_useful_life) >= flt(self.gross_purchase_amount):
frappe.throw(_("Row {0}: Expected Value After Useful Life must be less than Gross Purchase Amount")
@ -402,6 +435,7 @@ class Asset(AccountsController):
# to ensure that final accumulated depreciation amount is accurate
def get_adjusted_depreciation_amount(self, depreciation_amount_without_pro_rata, depreciation_amount_for_last_row, finance_book):
if not self.opening_accumulated_depreciation:
depreciation_amount_for_first_row = self.get_depreciation_amount_for_first_row(finance_book)
if depreciation_amount_for_first_row + depreciation_amount_for_last_row != depreciation_amount_without_pro_rata:
@ -723,14 +757,14 @@ def create_asset_repair(asset, asset_name):
return asset_repair
@frappe.whitelist()
def create_asset_adjustment(asset, asset_category, company):
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
asset_maintenance.update({
def create_asset_value_adjustment(asset, asset_category, company):
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
asset_value_adjustment.update({
"asset": asset,
"company": company,
"asset_category": asset_category
})
return asset_maintenance
return asset_value_adjustment
@frappe.whitelist()
def transfer_asset(args):
@ -850,13 +884,11 @@ def get_total_days(date, frequency):
@erpnext.allow_regional
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"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
depreciation_amount = (flt(asset.gross_purchase_amount) -
flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:

View File

@ -57,8 +57,10 @@ def make_depreciation_entry(asset_name, date=None):
je.finance_book = d.finance_book
je.remark = "Depreciation Entry against {0} worth {1}".format(asset_name, d.depreciation_amount)
credit_account, debit_account = get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account)
credit_entry = {
"account": accumulated_depreciation_account,
"account": credit_account,
"credit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@ -66,7 +68,7 @@ def make_depreciation_entry(asset_name, date=None):
}
debit_entry = {
"account": depreciation_expense_account,
"account": debit_account,
"debit_in_account_currency": d.depreciation_amount,
"reference_type": "Asset",
"reference_name": asset.name,
@ -132,6 +134,20 @@ def get_depreciation_accounts(asset):
return fixed_asset_account, accumulated_depreciation_account, depreciation_expense_account
def get_credit_and_debit_accounts(accumulated_depreciation_account, depreciation_expense_account):
root_type = frappe.get_value("Account", depreciation_expense_account, "root_type")
if root_type == "Expense":
credit_account = accumulated_depreciation_account
debit_account = depreciation_expense_account
elif root_type == "Income":
credit_account = depreciation_expense_account
debit_account = accumulated_depreciation_account
else:
frappe.throw(_("Depreciation Expense Account should be an Income or Expense Account."))
return credit_account, debit_account
@frappe.whitelist()
def scrap_asset(asset_name):
asset = frappe.get_doc("Asset", asset_name)

View File

@ -409,19 +409,18 @@ class TestDepreciationMethods(AssetSetup):
calculate_depreciation = 1,
available_for_use_date = "2030-06-06",
is_existing_asset = 1,
number_of_depreciations_booked = 1,
opening_accumulated_depreciation = 40000,
number_of_depreciations_booked = 2,
opening_accumulated_depreciation = 47095.89,
expected_value_after_useful_life = 10000,
depreciation_start_date = "2030-12-31",
depreciation_start_date = "2032-12-31",
total_number_of_depreciations = 3,
frequency_of_depreciation = 12
)
self.assertEqual(asset.status, "Draft")
expected_schedules = [
["2030-12-31", 14246.58, 54246.58],
["2031-12-31", 25000.00, 79246.58],
["2032-06-06", 10753.42, 90000.00]
["2032-12-31", 30000.0, 77095.89],
["2033-06-06", 12904.11, 90000.0]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount]
for d in asset.get("schedules")]
@ -869,6 +868,72 @@ class TestDepreciationBasics(AssetSetup):
self.assertFalse(asset.schedules[1].journal_entry)
self.assertFalse(asset.schedules[2].journal_entry)
def test_depr_entry_posting_when_depr_expense_account_is_an_expense_account(self):
"""Tests if the Depreciation Expense Account gets debited and the Accumulated Depreciation Account gets credited when the former's an Expense Account."""
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
depreciation_start_date = "2020-12-31",
frequency_of_depreciation = 12,
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
submit = 1
)
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
for entry in accounting_entries:
if entry["account"] == "_Test Depreciations - _TC":
self.assertTrue(entry["debit"])
self.assertFalse(entry["credit"])
else:
self.assertTrue(entry["credit"])
self.assertFalse(entry["debit"])
def test_depr_entry_posting_when_depr_expense_account_is_an_income_account(self):
"""Tests if the Depreciation Expense Account gets credited and the Accumulated Depreciation Account gets debited when the former's an Income Account."""
depr_expense_account = frappe.get_doc("Account", "_Test Depreciations - _TC")
depr_expense_account.root_type = "Income"
depr_expense_account.parent_account = "Income - _TC"
depr_expense_account.save()
asset = create_asset(
item_code = "Macbook Pro",
calculate_depreciation = 1,
available_for_use_date = "2019-12-31",
depreciation_start_date = "2020-12-31",
frequency_of_depreciation = 12,
total_number_of_depreciations = 3,
expected_value_after_useful_life = 10000,
submit = 1
)
post_depreciation_entries(date="2021-06-01")
asset.load_from_db()
je = frappe.get_doc("Journal Entry", asset.schedules[0].journal_entry)
accounting_entries = [{"account": entry.account, "debit": entry.debit, "credit": entry.credit} for entry in je.accounts]
for entry in accounting_entries:
if entry["account"] == "_Test Depreciations - _TC":
self.assertTrue(entry["credit"])
self.assertFalse(entry["debit"])
else:
self.assertTrue(entry["debit"])
self.assertFalse(entry["credit"])
# resetting
depr_expense_account.root_type = "Expense"
depr_expense_account.parent_account = "Expenses - _TC"
depr_expense_account.save()
def test_clear_depreciation_schedule(self):
"""Tests if clear_depreciation_schedule() works as expected."""
@ -890,6 +955,82 @@ class TestDepreciationBasics(AssetSetup):
self.assertEqual(len(asset.schedules), 1)
def test_clear_depreciation_schedule_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 1,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-01-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.submit()
post_depreciation_entries(date="2020-04-01")
asset.load_from_db()
asset.clear_depreciation_schedule()
self.assertEqual(len(asset.schedules), 6)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, "1")
else:
self.assertEqual(schedule.finance_book_id, "2")
def test_depreciation_schedules_are_set_up_for_multiple_finance_books(self):
asset = create_asset(
item_code = "Macbook Pro",
available_for_use_date = "2019-12-31",
do_not_save = 1
)
asset.calculate_depreciation = 1
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 3,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.append("finance_books", {
"depreciation_method": "Straight Line",
"frequency_of_depreciation": 12,
"total_number_of_depreciations": 6,
"expected_value_after_useful_life": 10000,
"depreciation_start_date": "2020-12-31"
})
asset.save()
self.assertEqual(len(asset.schedules), 9)
for schedule in asset.schedules:
if schedule.idx <= 3:
self.assertEqual(schedule.finance_book_id, 1)
else:
self.assertEqual(schedule.finance_book_id, 2)
def test_depreciation_entry_cancellation(self):
asset = create_asset(
item_code = "Macbook Pro",

View File

@ -33,7 +33,7 @@ frappe.ui.form.on('Asset Category', {
var d = locals[cdt][cdn];
return {
"filters": {
"root_type": "Expense",
"root_type": ["in", ["Expense", "Income"]],
"is_group": 0,
"company": d.company_name
}

View File

@ -42,10 +42,10 @@ class AssetCategory(Document):
def validate_account_types(self):
account_type_map = {
'fixed_asset_account': { 'account_type': 'Fixed Asset' },
'accumulated_depreciation_account': { 'account_type': 'Accumulated Depreciation' },
'depreciation_expense_account': { 'root_type': 'Expense' },
'capital_work_in_progress_account': { 'account_type': 'Capital Work in Progress' }
'fixed_asset_account': {'account_type': ['Fixed Asset']},
'accumulated_depreciation_account': {'account_type': ['Accumulated Depreciation']},
'depreciation_expense_account': {'root_type': ['Expense', 'Income']},
'capital_work_in_progress_account': {'account_type': ['Capital Work in Progress']}
}
for d in self.accounts:
for fieldname in account_type_map.keys():
@ -53,11 +53,11 @@ class AssetCategory(Document):
selected_account = d.get(fieldname)
key_to_match = next(iter(account_type_map.get(fieldname))) # acount_type or root_type
selected_key_type = frappe.db.get_value('Account', selected_account, key_to_match)
expected_key_type = account_type_map[fieldname][key_to_match]
expected_key_types = account_type_map[fieldname][key_to_match]
if selected_key_type != expected_key_type:
if selected_key_type not in expected_key_types:
frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.")
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)),
.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_types)),
title=_("Invalid Account"))
def valide_cwip_account(self):

View File

@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController):
self.create_raw_materials_supplied("supplied_items")
self.set_received_qty_for_drop_ship_items()
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
self.reset_default_field_value("set_warehouse", "items", "warehouse")
def validate_with_previous_doc(self):
super(PurchaseOrder, self).validate_with_previous_doc({

View File

@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show()
},
schedule_date(frm) {
if(frm.doc.schedule_date){
frm.doc.items.forEach((item) => {
item.schedule_date = frm.doc.schedule_date;
})
}
refresh_field("items");
},
preview: (frm) => {
let dialog = new frappe.ui.Dialog({
title: __('Preview Email'),
@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
dialog.show();
}
})
frappe.ui.form.on("Request for Quotation Item", {
items_add(frm, cdt, cdn) {
if (frm.doc.schedule_date) {
frappe.model.set_value(cdt, cdn, 'schedule_date', frm.doc.schedule_date);
}
}
});
frappe.ui.form.on("Request for Quotation Supplier",{
supplier: function(frm, cdt, cdn) {
var d = locals[cdt][cdn]

View File

@ -12,6 +12,7 @@
"vendor",
"column_break1",
"transaction_date",
"schedule_date",
"status",
"amended_from",
"suppliers_section",
@ -246,16 +247,22 @@
"fieldname": "sec_break_email_2",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "schedule_date",
"fieldtype": "Date",
"label": "Required Date"
}
],
"icon": "fa fa-shopping-cart",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-11-05 22:04:29.017134",
"modified": "2021-11-24 17:47:49.909000",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -17,6 +17,7 @@
"company",
"transaction_date",
"valid_till",
"quotation_number",
"amended_from",
"address_section",
"supplier_address",
@ -797,6 +798,11 @@
"fieldtype": "Date",
"in_list_view": 1,
"label": "Valid Till"
},
{
"fieldname": "quotation_number",
"fieldtype": "Data",
"label": "Quotation Number"
}
],
"icon": "fa fa-shopping-cart",
@ -804,10 +810,11 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-04-19 00:58:20.995491",
"modified": "2021-12-11 06:43:20.924080",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -250,7 +250,12 @@ class AccountsController(TransactionBase):
from erpnext.controllers.taxes_and_totals import calculate_taxes_and_totals
calculate_taxes_and_totals(self)
if self.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
if self.doctype in (
'Sales Order',
'Delivery Note',
'Sales Invoice',
'POS Invoice',
):
self.calculate_commission()
self.calculate_contribution()

View File

@ -539,6 +539,10 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
query_filters = []
or_filters = []
fields = ['name']
searchfields = frappe.get_meta(doctype).get_search_fields()
meta = frappe.get_meta(doctype)
if meta.is_tree:
@ -550,8 +554,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
if meta.has_field('company'):
query_filters.append(['company', '=', filters.get('company')])
if txt:
query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
for field in searchfields:
or_filters.append([field, 'LIKE', "%%%s%%" % txt])
fields.append(field)
if dimension_filters:
if dimension_filters['allow_or_restrict'] == 'Allow':
@ -566,10 +571,9 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
query_filters.append(['name', query_selector, dimensions])
output = frappe.get_list(doctype, filters=query_filters)
result = [d.name for d in output]
output = frappe.get_list(doctype, fields=fields, filters=query_filters, or_filters=or_filters, as_list=1)
return [(d,) for d in set(result)]
return [tuple(d) for d in set(output)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@ -120,13 +120,27 @@ class SellingController(StockController):
self.in_words = money_in_words(amount, self.currency)
def calculate_commission(self):
if self.meta.get_field("commission_rate"):
self.round_floats_in(self, ["base_net_total", "commission_rate"])
if self.commission_rate > 100.0:
throw(_("Commission rate cannot be greater than 100"))
if not self.meta.get_field("commission_rate"):
return
self.total_commission = flt(self.base_net_total * self.commission_rate / 100.0,
self.precision("total_commission"))
self.round_floats_in(
self, ("amount_eligible_for_commission", "commission_rate")
)
if not (0 <= self.commission_rate <= 100.0):
throw("{} {}".format(
_(self.meta.get_label("commission_rate")),
_("must be between 0 and 100"),
))
self.amount_eligible_for_commission = sum(
item.base_net_amount for item in self.items if item.grant_commission
)
self.total_commission = flt(
self.amount_eligible_for_commission * self.commission_rate / 100.0,
self.precision("total_commission")
)
def calculate_contribution(self):
if not self.meta.get_field("sales_team"):
@ -138,7 +152,7 @@ class SellingController(StockController):
self.round_floats_in(sales_person)
sales_person.allocated_amount = flt(
self.base_net_total * sales_person.allocated_percentage / 100.0,
self.amount_eligible_for_commission * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
if sales_person.commission_rate:

View File

@ -0,0 +1,22 @@
import unittest
import frappe
class TestUtils(unittest.TestCase):
def test_reset_default_field_value(self):
doc = frappe.get_doc({
"doctype": "Purchase Receipt",
"set_warehouse": "Warehouse 1",
})
# Same values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, "Warehouse 1")
# Mixed values
doc.items = [{"warehouse": "Warehouse 1"}, {"warehouse": "Warehouse 2"}, {"warehouse": "Warehouse 1"}]
doc.reset_default_field_value("set_warehouse", "items", "warehouse")
self.assertEqual(doc.set_warehouse, None)

View File

@ -0,0 +1,8 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('CRM Settings', {
// refresh: function(frm) {
// }
});

View File

@ -0,0 +1,113 @@
{
"actions": [],
"creation": "2021-09-09 17:03:22.754446",
"description": "Settings for Selling Module",
"doctype": "DocType",
"document_type": "Other",
"engine": "InnoDB",
"field_order": [
"section_break_5",
"campaign_naming_by",
"allow_lead_duplication_based_on_emails",
"column_break_4",
"create_event_on_next_contact_date",
"auto_creation_of_contact",
"opportunity_section",
"close_opportunity_after_days",
"column_break_9",
"create_event_on_next_contact_date_opportunity",
"quotation_section",
"default_valid_till"
],
"fields": [
{
"fieldname": "campaign_naming_by",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Campaign Naming By",
"options": "Campaign Name\nNaming Series"
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "default_valid_till",
"fieldtype": "Data",
"label": "Default Quotation Validity Days"
},
{
"fieldname": "section_break_5",
"fieldtype": "Section Break",
"label": "Lead"
},
{
"default": "0",
"fieldname": "allow_lead_duplication_based_on_emails",
"fieldtype": "Check",
"label": "Allow Lead Duplication based on Emails"
},
{
"default": "1",
"fieldname": "auto_creation_of_contact",
"fieldtype": "Check",
"label": "Auto Creation of Contact"
},
{
"default": "1",
"fieldname": "create_event_on_next_contact_date",
"fieldtype": "Check",
"label": "Create Event on Next Contact Date"
},
{
"fieldname": "opportunity_section",
"fieldtype": "Section Break",
"label": "Opportunity"
},
{
"default": "15",
"description": "Auto close Opportunity Replied after the no. of days mentioned above",
"fieldname": "close_opportunity_after_days",
"fieldtype": "Int",
"label": "Close Replied Opportunity After Days"
},
{
"default": "1",
"fieldname": "create_event_on_next_contact_date_opportunity",
"fieldtype": "Check",
"label": "Create Event on Next Contact Date"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "quotation_section",
"fieldtype": "Section Break",
"label": "Quotation"
}
],
"icon": "fa fa-cog",
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-11-03 10:00:36.883496",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# 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 CRMSettings(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
import unittest
class TestCRMSettings(unittest.TestCase):
pass

View File

@ -11,6 +11,7 @@ from frappe.utils import (
cint,
comma_and,
cstr,
get_link_to_form,
getdate,
has_gravatar,
nowdate,
@ -91,6 +92,7 @@ class Lead(SellingController):
self.contact_doc.save()
def add_calendar_event(self, opts=None, force=False):
if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date'):
super(Lead, self).add_calendar_event({
"owner": self.lead_owner,
"starts_on": self.contact_date,
@ -108,8 +110,9 @@ class Lead(SellingController):
def check_email_id_is_unique(self):
if self.email_id:
# validate email is unique
if not frappe.db.get_single_value('CRM Settings', 'allow_lead_duplication_based_on_emails'):
duplicate_leads = frappe.get_all("Lead", filters={"email_id": self.email_id, "name": ["!=", self.name]})
duplicate_leads = [lead.name for lead in duplicate_leads]
duplicate_leads = [frappe.bold(get_link_to_form('Lead', lead.name)) for lead in duplicate_leads]
if duplicate_leads:
frappe.throw(_("Email Address must be unique, already exists for {0}")
@ -172,6 +175,7 @@ class Lead(SellingController):
self.title = self.company_name or self.lead_name
def create_contact(self):
if frappe.db.get_single_value('CRM Settings', 'auto_creation_of_contact'):
if not self.lead_name:
self.set_full_name()
self.set_lead_name()

View File

@ -510,8 +510,7 @@
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
"modified": "2021-10-21 11:04:30.151379",
"modified": "2021-10-21 12:04:30.151379",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",

View File

@ -8,6 +8,7 @@ import frappe
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.query_builder import DocType
from frappe.utils import cint, cstr, flt, get_fullname
from erpnext.crm.utils import add_link_in_communication, copy_comments
@ -33,7 +34,6 @@ class Opportunity(TransactionBase):
})
self.make_new_lead_if_required()
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
@ -75,21 +75,21 @@ class Opportunity(TransactionBase):
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:
# check if customer is already created agains the self.contact_email
customer = frappe.db.sql("""select
distinct `tabDynamic Link`.link_name as customer
from
`tabContact`,
`tabDynamic Link`
where `tabContact`.email_id='{0}'
and
`tabContact`.name=`tabDynamic Link`.parent
and
ifnull(`tabDynamic Link`.link_name, '')<>''
and
`tabDynamic Link`.link_doctype='Customer'
""".format(self.contact_email), as_dict=True)
if customer and customer[0].customer:
self.party_name = customer[0].customer
dynamic_link, contact = DocType("Dynamic Link"), DocType("Contact")
customer = frappe.qb.from_(
dynamic_link
).join(
contact
).on(
(contact.name == dynamic_link.parent)
& (dynamic_link.link_doctype == "Customer")
& (contact.email_id == self.contact_email)
).select(
dynamic_link.link_name
).distinct().run(as_dict=True)
if customer and customer[0].link_name:
self.party_name = customer[0].link_name
self.opportunity_from = "Customer"
return
@ -196,6 +196,7 @@ class Opportunity(TransactionBase):
self.add_calendar_event()
def add_calendar_event(self, opts=None, force=False):
if frappe.db.get_single_value('CRM Settings', 'create_event_on_next_contact_date_opportunity'):
if not opts:
opts = frappe._dict()
@ -368,7 +369,7 @@ def set_multiple_status(names, status):
def auto_close_opportunity():
""" auto close the `Replied` Opportunities after 7 days """
auto_close_after_days = frappe.db.get_single_value("Selling Settings", "close_opportunity_after_days") or 15
auto_close_after_days = frappe.db.get_single_value("CRM Settings", "close_opportunity_after_days") or 15
opportunities = frappe.db.sql(""" select name from tabOpportunity where status='Replied' and
modified<DATE_SUB(CURDATE(), INTERVAL %s DAY) """, (auto_close_after_days), as_dict=True)

View File

@ -20,7 +20,6 @@
"configuration_cb",
"shipping_account_head",
"section_break_12",
"nexus_address",
"nexus"
],
"fields": [
@ -87,15 +86,11 @@
"fieldtype": "Column Break"
},
{
"depends_on": "nexus",
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Nexus List"
},
{
"fieldname": "nexus_address",
"fieldtype": "HTML",
"label": "Nexus Address"
},
{
"fieldname": "nexus",
"fieldtype": "Table",
@ -107,20 +102,20 @@
"fieldname": "configuration_cb",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
}
],
"issingle": 1,
"links": [],
"modified": "2021-11-08 18:02:29.232090",
"modified": "2021-11-30 12:17:24.647979",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",

View File

@ -16,9 +16,9 @@ from erpnext.erpnext_integrations.taxjar_integration import get_client
class TaxJarSettings(Document):
def on_update(self):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
TAXJAR_CREATE_TRANSACTIONS = self.taxjar_create_transactions
TAXJAR_CALCULATE_TAX = self.taxjar_calculate_tax
TAXJAR_SANDBOX_MODE = self.is_sandbox
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')

View File

@ -234,7 +234,7 @@ doc_events = {
},
"Communication": {
"on_update": [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.on_communication_update",
"erpnext.support.doctype.issue.issue.set_first_response_time"
]
},
@ -265,6 +265,9 @@ doc_events = {
"erpnext.regional.india.utils.update_taxable_values"
]
},
"POS Invoice": {
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
},
"Purchase Invoice": {
"validate": [
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
@ -340,8 +343,7 @@ scheduler_events = {
"erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts"
],
"hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"

View File

@ -96,15 +96,8 @@ class Employee(NestedSet):
'user': self.user_id
})
if employee_user_permission_exists: return
employee_user_permission_exists = frappe.db.exists('User Permission', {
'allow': 'Employee',
'for_value': self.name,
'user': self.user_id
})
if employee_user_permission_exists: return
if employee_user_permission_exists:
return
add_user_permission("Employee", self.name, self.user_id)
set_user_permission_if_allowed("Company", self.company, self.user_id)

View File

@ -21,7 +21,11 @@ def get_data():
},
{
'label': _('Lifecycle'),
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Separation', 'Employee Grievance']
'items': ['Employee Transfer', 'Employee Promotion', 'Employee Grievance']
},
{
'label': _('Exit'),
'items': ['Employee Separation', 'Exit Interview', 'Full and Final Statement']
},
{
'label': _('Shift'),

View File

@ -2,7 +2,6 @@
# See license.txt
import unittest
from datetime import date
import frappe
from frappe.utils import add_days, getdate
@ -12,16 +11,14 @@ from erpnext.hr.doctype.employee.test_employee import make_employee
class TestEmployeeTransfer(unittest.TestCase):
def setUp(self):
make_employee("employee2@transfers.com")
make_employee("employee3@transfers.com")
create_company()
create_employee()
create_employee_transfer()
def tearDown(self):
frappe.db.rollback()
def test_submit_before_transfer_date(self):
make_employee("employee2@transfers.com")
transfer_obj = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee2@transfers.com"}, "name"),
@ -43,6 +40,8 @@ class TestEmployeeTransfer(unittest.TestCase):
self.assertEqual(transfer.docstatus, 1)
def test_new_employee_creation(self):
make_employee("employee3@transfers.com")
transfer = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"user_id":"employee3@transfers.com"}, "name"),
@ -63,60 +62,51 @@ class TestEmployeeTransfer(unittest.TestCase):
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
def test_employee_history(self):
name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
doc = frappe.get_doc("Employee",name)
employee = make_employee("employee4@transfers.com",
company="Test Company",
date_of_birth=getdate("30-09-1980"),
date_of_joining=getdate("01-10-2021"),
department="Accounts - TC",
designation="Accountant"
)
transfer = create_employee_transfer(employee)
count = 0
department = ["Accounts - TC", "Management - TC"]
designation = ["Accountant", "Manager"]
dt = [getdate("01-10-2021"), date.today()]
dt = [getdate("01-10-2021"), getdate()]
for data in doc.internal_work_history:
employee = frappe.get_doc("Employee", employee)
for data in employee.internal_work_history:
self.assertEqual(data.department, department[count])
self.assertEqual(data.designation, designation[count])
self.assertEqual(data.from_date, dt[count])
count = count + 1
data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
doc = frappe.get_doc("Employee Transfer", data[0]["name"])
doc.cancel()
employee_doc = frappe.get_doc("Employee",name)
transfer.cancel()
employee.reload()
for data in employee_doc.internal_work_history:
for data in employee.internal_work_history:
self.assertEqual(data.designation, designation[0])
self.assertEqual(data.department, department[0])
self.assertEqual(data.from_date, dt[0])
def create_employee():
doc = frappe.get_doc({
"doctype": "Employee",
"first_name": "John",
"company": "Test Company",
"gender": "Male",
"date_of_birth": getdate("30-09-1980"),
"date_of_joining": getdate("01-10-2021"),
"department": "Accounts - TC",
"designation": "Accountant"
})
doc.save()
def create_company():
exists = frappe.db.exists("Company", "Test Company")
if not exists:
doc = frappe.get_doc({
if not frappe.db.exists("Company", "Test Company"):
frappe.get_doc({
"doctype": "Company",
"company_name": "Test Company",
"default_currency": "INR",
"country": "India"
})
}).insert()
doc.save()
def create_employee_transfer():
def create_employee_transfer(employee):
doc = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
"transfer_date": date.today(),
"employee": employee,
"transfer_date": getdate(),
"transfer_details": [
{
"property": "Designation",
@ -135,3 +125,5 @@ def create_employee_transfer():
doc.save()
doc.submit()
return doc

View File

@ -0,0 +1,38 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Exit Interview', {
refresh: function(frm) {
if (!frm.doc.__islocal && !frm.doc.questionnaire_email_sent && frappe.boot.user.can_write.includes('Exit Interview')) {
frm.add_custom_button(__('Send Exit Questionnaire'), function () {
frm.trigger('send_exit_questionnaire');
});
}
},
employee: function(frm) {
frappe.db.get_value('Employee', frm.doc.employee, 'relieving_date', (message) => {
if (!message.relieving_date) {
frappe.throw({
message: __('Please set the relieving date for employee {0}',
['<a href="/app/employee/' + frm.doc.employee +'">' + frm.doc.employee + '</a>']),
title: __('Relieving Date Missing')
});
}
});
},
send_exit_questionnaire: function(frm) {
frappe.call({
method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
args: {
'interviews': [frm.doc]
},
callback: function(r) {
if (!r.exc) {
frm.refresh_field('questionnaire_email_sent');
}
}
});
}
});

View File

@ -0,0 +1,246 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "naming_series:",
"creation": "2021-12-05 13:56:36.241690",
"doctype": "DocType",
"editable_grid": 1,
"email_append_to": 1,
"engine": "InnoDB",
"field_order": [
"naming_series",
"employee",
"employee_name",
"email",
"column_break_5",
"company",
"status",
"date",
"employee_details_section",
"department",
"designation",
"reports_to",
"column_break_9",
"date_of_joining",
"relieving_date",
"exit_questionnaire_section",
"ref_doctype",
"questionnaire_email_sent",
"column_break_10",
"reference_document_name",
"interview_summary_section",
"interviewers",
"interview_summary",
"employee_status_section",
"employee_status",
"amended_from"
],
"fields": [
{
"fieldname": "employee",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Employee",
"options": "Employee",
"reqd": 1
},
{
"fetch_from": "employee.employee_name",
"fieldname": "employee_name",
"fieldtype": "Data",
"label": "Employee Name",
"read_only": 1
},
{
"fetch_from": "employee.department",
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department",
"read_only": 1
},
{
"fetch_from": "employee.relieving_date",
"fieldname": "relieving_date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Relieving Date",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "date",
"fieldtype": "Date",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Date",
"mandatory_depends_on": "eval:doc.status==='Scheduled';"
},
{
"fieldname": "exit_questionnaire_section",
"fieldtype": "Section Break",
"label": "Exit Questionnaire"
},
{
"fieldname": "ref_doctype",
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType"
},
{
"fieldname": "reference_document_name",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Document Name",
"options": "ref_doctype"
},
{
"fieldname": "interview_summary_section",
"fieldtype": "Section Break",
"label": "Interview Details"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "interviewers",
"fieldtype": "Table MultiSelect",
"label": "Interviewers",
"mandatory_depends_on": "eval:doc.status==='Scheduled';",
"options": "Interviewer"
},
{
"fetch_from": "employee.date_of_joining",
"fieldname": "date_of_joining",
"fieldtype": "Date",
"label": "Date of Joining",
"read_only": 1
},
{
"fetch_from": "employee.reports_to",
"fieldname": "reports_to",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Reports To",
"options": "Employee",
"read_only": 1
},
{
"fieldname": "employee_details_section",
"fieldtype": "Section Break",
"label": "Employee Details"
},
{
"fetch_from": "employee.designation",
"fieldname": "designation",
"fieldtype": "Link",
"label": "Designation",
"options": "Designation",
"read_only": 1
},
{
"fieldname": "column_break_9",
"fieldtype": "Column Break"
},
{
"fieldname": "naming_series",
"fieldtype": "Select",
"label": "Naming Series",
"options": "HR-EXIT-INT-"
},
{
"default": "0",
"fieldname": "questionnaire_email_sent",
"fieldtype": "Check",
"in_standard_filter": 1,
"label": "Questionnaire Email Sent",
"no_copy": 1,
"read_only": 1
},
{
"fieldname": "email",
"fieldtype": "Data",
"label": "Email ID",
"options": "Email",
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
"options": "Pending\nScheduled\nCompleted\nCancelled",
"reqd": 1
},
{
"fieldname": "employee_status_section",
"fieldtype": "Section Break"
},
{
"fieldname": "employee_status",
"fieldtype": "Select",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Final Decision",
"mandatory_depends_on": "eval:doc.status==='Completed';",
"options": "\nEmployee Retained\nExit Confirmed"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Exit Interview",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "interview_summary",
"fieldtype": "Text Editor",
"label": "Interview Summary"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-12-07 23:39:22.645401",
"modified_by": "Administrator",
"module": "HR",
"name": "Exit Interview",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sender_field": "email",
"sort_field": "modified",
"sort_order": "DESC",
"title_field": "employee_name",
"track_changes": 1
}

View File

@ -0,0 +1,131 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils import get_link_to_form
from erpnext.hr.doctype.employee.employee import get_employee_email
class ExitInterview(Document):
def validate(self):
self.validate_relieving_date()
self.validate_duplicate_interview()
self.set_employee_email()
def validate_relieving_date(self):
if not frappe.db.get_value('Employee', self.employee, 'relieving_date'):
frappe.throw(_('Please set the relieving date for employee {0}').format(
get_link_to_form('Employee', self.employee)),
title=_('Relieving Date Missing'))
def validate_duplicate_interview(self):
doc = frappe.db.exists('Exit Interview', {
'employee': self.employee,
'name': ('!=', self.name),
'docstatus': ('!=', 2)
})
if doc:
frappe.throw(_('Exit Interview {0} already exists for Employee: {1}').format(
get_link_to_form('Exit Interview', doc), frappe.bold(self.employee)),
frappe.DuplicateEntryError)
def set_employee_email(self):
employee = frappe.get_doc('Employee', self.employee)
self.email = get_employee_email(employee)
def on_submit(self):
if self.status != 'Completed':
frappe.throw(_('Only Completed documents can be submitted'))
self.update_interview_date_in_employee()
def on_cancel(self):
self.update_interview_date_in_employee()
self.db_set('status', 'Cancelled')
def update_interview_date_in_employee(self):
if self.docstatus == 1:
frappe.db.set_value('Employee', self.employee, 'held_on', self.date)
elif self.docstatus == 2:
frappe.db.set_value('Employee', self.employee, 'held_on', None)
@frappe.whitelist()
def send_exit_questionnaire(interviews):
interviews = get_interviews(interviews)
validate_questionnaire_settings()
email_success = []
email_failure = []
for exit_interview in interviews:
interview = frappe.get_doc('Exit Interview', exit_interview.get('name'))
if interview.get('questionnaire_email_sent'):
continue
employee = frappe.get_doc('Employee', interview.employee)
email = get_employee_email(employee)
context = interview.as_dict()
context.update(employee.as_dict())
template_name = frappe.db.get_single_value('HR Settings', 'exit_questionnaire_notification_template')
template = frappe.get_doc('Email Template', template_name)
if email:
frappe.sendmail(
recipients=email,
subject=template.subject,
message=frappe.render_template(template.response, context),
reference_doctype=interview.doctype,
reference_name=interview.name
)
interview.db_set('questionnaire_email_sent', True)
interview.notify_update()
email_success.append(email)
else:
email_failure.append(get_link_to_form('Employee', employee.name))
show_email_summary(email_success, email_failure)
def get_interviews(interviews):
import json
if isinstance(interviews, str):
interviews = json.loads(interviews)
if not len(interviews):
frappe.throw(_('Atleast one interview has to be selected.'))
return interviews
def validate_questionnaire_settings():
settings = frappe.db.get_value('HR Settings', 'HR Settings',
['exit_questionnaire_web_form', 'exit_questionnaire_notification_template'], as_dict=True)
if not settings.exit_questionnaire_web_form or not settings.exit_questionnaire_notification_template:
frappe.throw(
_('Please set {0} and {1} in {2}.').format(
frappe.bold('Exit Questionnaire Web Form'),
frappe.bold('Notification Template'),
get_link_to_form('HR Settings', 'HR Settings')),
title=_('Settings Missing')
)
def show_email_summary(email_success, email_failure):
message = ''
if email_success:
message += _('{0}: {1}').format(
frappe.bold('Sent Successfully'), ', '.join(email_success))
if message and email_failure:
message += '<br><br>'
if email_failure:
message += _('{0} due to missing email information for employee(s): {1}').format(
frappe.bold('Sending Failed'), ', '.join(email_failure))
frappe.msgprint(message, title=_('Exit Questionnaire'), indicator='blue', is_minimizable=True, wide=True)

View File

@ -0,0 +1,27 @@
frappe.listview_settings['Exit Interview'] = {
has_indicator_for_draft: 1,
get_indicator: function(doc) {
let status_color = {
'Pending': 'orange',
'Scheduled': 'yellow',
'Completed': 'green',
'Cancelled': 'red',
};
return [__(doc.status), status_color[doc.status], 'status,=,'+doc.status];
},
onload: function(listview) {
if (frappe.boot.user.can_write.includes('Exit Interview')) {
listview.page.add_action_item(__('Send Exit Questionnaires'), function() {
const interviews = listview.get_checked_items();
frappe.call({
method: 'erpnext.hr.doctype.exit_interview.exit_interview.send_exit_questionnaire',
freeze: true,
args: {
'interviews': interviews
}
});
});
}
}
};

View File

@ -0,0 +1,16 @@
<h2>Exit Questionnaire</h2>
<br>
<p>
Dear {{ employee_name }},
<br><br>
Thank you for the contribution you have made during your time at {{ company }}. We value your opinion and welcome the feedback on your experience working with us.
Request you to take out a few minutes to fill up this Exit Questionnaire.
{% set web_form = frappe.db.get_value('HR Settings', 'HR Settings', 'exit_questionnaire_web_form') %}
{% set web_form_link = frappe.utils.get_url(uri=frappe.db.get_value('Web Form', web_form, 'route')) %}
<br><br>
<a class="btn btn-primary" href="{{ web_form_link }}" target="_blank">{{ _('Submit Now') }}</a>
</p>

View File

@ -0,0 +1,118 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import os
import unittest
import frappe
from frappe import _
from frappe.core.doctype.user_permission.test_user_permission import create_user
from frappe.tests.test_webform import create_custom_doctype, create_webform
from frappe.utils import getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.exit_interview.exit_interview import send_exit_questionnaire
class TestExitInterview(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabExit Interview`')
def test_duplicate_interview(self):
employee = make_employee('employeeexit1@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
doc = frappe.copy_doc(interview)
self.assertRaises(frappe.DuplicateEntryError, doc.save)
def test_relieving_date_validation(self):
employee = make_employee('employeeexit2@example.com')
# unset relieving date
frappe.db.set_value('Employee', employee, 'relieving_date', None)
interview = create_exit_interview(employee, save=False)
self.assertRaises(frappe.ValidationError, interview.save)
# set relieving date
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
self.assertTrue(interview.name)
def test_interview_date_updated_in_employee_master(self):
employee = make_employee('employeeexit3@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
interview.status = 'Completed'
interview.employee_status = 'Exit Confirmed'
# exit interview date updated on submit
interview.submit()
self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), interview.date)
# exit interview reset on cancel
interview.reload()
interview.cancel()
self.assertEqual(frappe.db.get_value('Employee', employee, 'held_on'), None)
def test_send_exit_questionnaire(self):
create_custom_doctype()
create_webform()
template = create_notification_template()
webform = frappe.db.get_all('Web Form', limit=1)
frappe.db.set_value('HR Settings', 'HR Settings', {
'exit_questionnaire_web_form': webform[0].name,
'exit_questionnaire_notification_template': template
})
employee = make_employee('employeeexit3@example.com')
frappe.db.set_value('Employee', employee, 'relieving_date', getdate())
interview = create_exit_interview(employee)
send_exit_questionnaire([interview])
email_queue = frappe.db.get_all('Email Queue', ['name', 'message'], limit=1)
self.assertTrue('Subject: Exit Questionnaire Notification' in email_queue[0].message)
def tearDown(self):
frappe.db.rollback()
def create_exit_interview(employee, save=True):
interviewer = create_user('test_exit_interviewer@example.com')
doc = frappe.get_doc({
'doctype': 'Exit Interview',
'employee': employee,
'company': '_Test Company',
'status': 'Pending',
'date': getdate(),
'interviewers': [{
'interviewer': interviewer.name
}],
'interview_summary': 'Test'
})
if save:
return doc.insert()
return doc
def create_notification_template():
template = frappe.db.exists('Email Template', _('Exit Questionnaire Notification'))
if not template:
base_path = frappe.get_app_path('erpnext', 'hr', 'doctype')
response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
template = frappe.get_doc({
'doctype': 'Email Template',
'name': _('Exit Questionnaire Notification'),
'response': response,
'subject': _('Exit Questionnaire Notification'),
'owner': frappe.session.user,
}).insert(ignore_permissions=True)
template = template.name
return template

View File

@ -36,7 +36,11 @@
"remind_before",
"column_break_4",
"send_interview_feedback_reminder",
"feedback_reminder_notification_template"
"feedback_reminder_notification_template",
"employee_exit_section",
"exit_questionnaire_web_form",
"column_break_34",
"exit_questionnaire_notification_template"
],
"fields": [
{
@ -226,13 +230,34 @@
"fieldname": "check_vacancies",
"fieldtype": "Check",
"label": "Check Vacancies On Job Offer Creation"
},
{
"fieldname": "employee_exit_section",
"fieldtype": "Section Break",
"label": "Employee Exit Settings"
},
{
"fieldname": "exit_questionnaire_web_form",
"fieldtype": "Link",
"label": "Exit Questionnaire Web Form",
"options": "Web Form"
},
{
"fieldname": "exit_questionnaire_notification_template",
"fieldtype": "Link",
"label": "Exit Questionnaire Notification Template",
"options": "Email Template"
},
{
"fieldname": "column_break_34",
"fieldtype": "Column Break"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-10-01 23:46:11.098236",
"modified": "2021-12-05 14:48:10.884253",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",

View File

@ -19,8 +19,8 @@ class ShiftAssignment(Document):
validate_active_employee(self.employee)
self.validate_overlapping_dates()
if self.end_date and self.end_date <= self.start_date:
frappe.throw(_("End Date must not be lesser than Start Date"))
if self.end_date:
self.validate_from_to_dates('start_date', 'end_date')
def validate_overlapping_dates(self):
if not self.name:

View File

@ -0,0 +1,29 @@
{
"attach_print": 0,
"channel": "Email",
"condition": "doc.date and doc.email and doc.docstatus != 2 and doc.status == 'Scheduled'",
"creation": "2021-12-05 22:11:47.263933",
"date_changed": "date",
"days_in_advance": 1,
"docstatus": 0,
"doctype": "Notification",
"document_type": "Exit Interview",
"enabled": 1,
"event": "Days Before",
"idx": 0,
"is_standard": 1,
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div class=\"text-medium text-muted\">\n\t\t\t\t<span>{{_(\"Exit Interview Scheduled:\")}} {{ doc.name }}</span>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n\t<tr height=\"10\"></tr>\n\t<tr>\n\t\t<td width=\"15\"></td>\n\t\t<td>\n\t\t\t<div>\n\t\t\t\t<ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n\t\t\t\t\t<li>{{_(\"Employee\")}}: <b>{{ doc.employee }} - {{ doc.employee_name }}</b></li>\n\t\t\t\t\t<li>{{_(\"Date\")}}: <b>{{ doc.date }}</b></li>\n\t\t\t\t\t<li> {{_(\"Interviewers\")}}: </li>\n\t\t\t\t\t{% for entry in doc.interviewers %}\n\t\t\t\t\t\t<ul>\n\t\t\t\t\t\t\t<li>{{ entry.user }}</li>\n\t\t\t\t\t\t</ul>\n\t\t\t\t\t{% endfor %}\n\t\t\t\t\t<li>{{ _(\"Interview Document\") }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n\t\t\t\t</ul>\n\t\t\t</div>\n\t\t</td>\n\t\t<td width=\"15\"></td>\n\t</tr>\n\t<tr height=\"10\"></tr>\n</table>\n",
"modified": "2021-12-05 22:26:57.096159",
"modified_by": "Administrator",
"module": "HR",
"name": "Exit Interview Scheduled",
"owner": "Administrator",
"recipients": [
{
"receiver_by_document_field": "email"
}
],
"send_system_notification": 0,
"send_to_all_assignees": 1,
"subject": "Exit Interview Scheduled: {{ doc.name }}"
}

View File

@ -0,0 +1,37 @@
<table class="panel-header" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr height="10"></tr>
<tr>
<td width="15"></td>
<td>
<div class="text-medium text-muted">
<h2>{{_("Exit Interview Scheduled:")}} {{ doc.name }}</h2>
</div>
</td>
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>
<table class="panel-body" border="0" cellpadding="0" cellspacing="0" width="100%">
<tr height="10"></tr>
<tr>
<td width="15"></td>
<td>
<div>
<ul class="list-unstyled" style="line-height: 1.7">
<li><b>{{_("Employee")}}: </b>{{ doc.employee }} - {{ doc.employee_name }}</li>
<li><b>{{_("Date")}}: </b>{{ frappe.utils.formatdate(doc.date) }}</li>
<li><b>{{_("Interviewers")}}:</b> </li>
{% for entry in doc.interviewers %}
<ul>
<li>{{ entry.user }}</li>
</ul>
{% endfor %}
<li><b>{{ _("Interview Document") }}:</b> {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
</ul>
</div>
</td>
<td width="15"></td>
</tr>
<tr height="10"></tr>
</table>

View File

@ -0,0 +1,6 @@
# import frappe
def get_context(context):
# do your magic here
pass

View File

@ -0,0 +1,77 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Employee Exits"] = {
filters: [
{
"fieldname": "from_date",
"label": __("From Date"),
"fieldtype": "Date",
"default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
},
{
"fieldname": "to_date",
"label": __("To Date"),
"fieldtype": "Date",
"default": frappe.datetime.nowdate()
},
{
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company"
},
{
"fieldname": "department",
"label": __("Department"),
"fieldtype": "Link",
"options": "Department"
},
{
"fieldname": "designation",
"label": __("Designation"),
"fieldtype": "Link",
"options": "Designation"
},
{
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "reports_to",
"label": __("Reports To"),
"fieldtype": "Link",
"options": "Employee"
},
{
"fieldname": "interview_status",
"label": __("Interview Status"),
"fieldtype": "Select",
"options": ["", "Pending", "Scheduled", "Completed"]
},
{
"fieldname": "final_decision",
"label": __("Final Decision"),
"fieldtype": "Select",
"options": ["", "Employee Retained", "Exit Confirmed"]
},
{
"fieldname": "exit_interview_pending",
"label": __("Exit Interview Pending"),
"fieldtype": "Check"
},
{
"fieldname": "questionnaire_pending",
"label": __("Exit Questionnaire Pending"),
"fieldtype": "Check"
},
{
"fieldname": "fnf_pending",
"label": __("FnF Pending"),
"fieldtype": "Check"
}
]
};

View File

@ -0,0 +1,33 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-12-05 19:47:18.332319",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"letter_head": "Test",
"modified": "2021-12-05 19:47:18.332319",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Exits",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Exit Interview",
"report_name": "Employee Exits",
"report_type": "Script Report",
"roles": [
{
"role": "System Manager"
},
{
"role": "HR Manager"
},
{
"role": "HR User"
}
]
}

View File

@ -0,0 +1,231 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# License: MIT. See LICENSE
import frappe
from frappe import _
from frappe.query_builder import Order
from frappe.utils import getdate
def execute(filters=None):
columns = get_columns()
data = get_data(filters)
chart = get_chart_data(data)
report_summary = get_report_summary(data)
return columns, data, None, chart, report_summary
def get_columns():
return [
{
'label': _('Employee'),
'fieldname': 'employee',
'fieldtype': 'Link',
'options': 'Employee',
'width': 150
},
{
'label': _('Employee Name'),
'fieldname': 'employee_name',
'fieldtype': 'Data',
'width': 150
},
{
'label': _('Date of Joining'),
'fieldname': 'date_of_joining',
'fieldtype': 'Date',
'width': 120
},
{
'label': _('Relieving Date'),
'fieldname': 'relieving_date',
'fieldtype': 'Date',
'width': 120
},
{
'label': _('Exit Interview'),
'fieldname': 'exit_interview',
'fieldtype': 'Link',
'options': 'Exit Interview',
'width': 150
},
{
'label': _('Interview Status'),
'fieldname': 'interview_status',
'fieldtype': 'Data',
'width': 130
},
{
'label': _('Final Decision'),
'fieldname': 'employee_status',
'fieldtype': 'Data',
'width': 150
},
{
'label': _('Full and Final Statement'),
'fieldname': 'full_and_final_statement',
'fieldtype': 'Link',
'options': 'Full and Final Statement',
'width': 180
},
{
'label': _('Department'),
'fieldname': 'department',
'fieldtype': 'Link',
'options': 'Department',
'width': 120
},
{
'label': _('Designation'),
'fieldname': 'designation',
'fieldtype': 'Link',
'options': 'Designation',
'width': 120
},
{
'label': _('Reports To'),
'fieldname': 'reports_to',
'fieldtype': 'Link',
'options': 'Employee',
'width': 120
}
]
def get_data(filters):
employee = frappe.qb.DocType('Employee')
interview = frappe.qb.DocType('Exit Interview')
fnf = frappe.qb.DocType('Full and Final Statement')
query = (
frappe.qb.from_(employee)
.left_join(interview).on(interview.employee == employee.name)
.left_join(fnf).on(fnf.employee == employee.name)
.select(
employee.name.as_('employee'), employee.employee_name.as_('employee_name'),
employee.date_of_joining.as_('date_of_joining'), employee.relieving_date.as_('relieving_date'),
employee.department.as_('department'), employee.designation.as_('designation'),
employee.reports_to.as_('reports_to'), interview.name.as_('exit_interview'),
interview.status.as_('interview_status'), interview.employee_status.as_('employee_status'),
interview.reference_document_name.as_('questionnaire'), fnf.name.as_('full_and_final_statement'))
.distinct()
.orderby(employee.relieving_date, order=Order.asc)
.where(
((employee.relieving_date.isnotnull()) | (employee.relieving_date != ''))
& ((interview.name.isnull()) | ((interview.name.isnotnull()) & (interview.docstatus != 2)))
& ((fnf.name.isnull()) | ((fnf.name.isnotnull()) & (fnf.docstatus != 2)))
)
)
query = get_conditions(filters, query, employee, interview, fnf)
result = query.run(as_dict=True)
return result
def get_conditions(filters, query, employee, interview, fnf):
if filters.get('from_date') and filters.get('to_date'):
query = query.where(employee.relieving_date[getdate(filters.get('from_date')):getdate(filters.get('to_date'))])
elif filters.get('from_date'):
query = query.where(employee.relieving_date >= filters.get('from_date'))
elif filters.get('to_date'):
query = query.where(employee.relieving_date <= filters.get('to_date'))
if filters.get('company'):
query = query.where(employee.company == filters.get('company'))
if filters.get('department'):
query = query.where(employee.department == filters.get('department'))
if filters.get('designation'):
query = query.where(employee.designation == filters.get('designation'))
if filters.get('employee'):
query = query.where(employee.name == filters.get('employee'))
if filters.get('reports_to'):
query = query.where(employee.reports_to == filters.get('reports_to'))
if filters.get('interview_status'):
query = query.where(interview.status == filters.get('interview_status'))
if filters.get('final_decision'):
query = query.where(interview.employee_status == filters.get('final_decision'))
if filters.get('exit_interview_pending'):
query = query.where((interview.name == '') | (interview.name.isnull()))
if filters.get('questionnaire_pending'):
query = query.where((interview.reference_document_name == '') | (interview.reference_document_name.isnull()))
if filters.get('fnf_pending'):
query = query.where((fnf.name == '') | (fnf.name.isnull()))
return query
def get_chart_data(data):
if not data:
return None
retained = 0
exit_confirmed = 0
pending = 0
for entry in data:
if entry.employee_status == 'Employee Retained':
retained += 1
elif entry.employee_status == 'Exit Confirmed':
exit_confirmed += 1
else:
pending += 1
chart = {
'data': {
'labels': [_('Retained'), _('Exit Confirmed'), _('Decision Pending')],
'datasets': [{'name': _('Employee Status'), 'values': [retained, exit_confirmed, pending]}]
},
'type': 'donut',
'colors': ['green', 'red', 'blue'],
}
return chart
def get_report_summary(data):
if not data:
return None
total_resignations = len(data)
interviews_pending = len([entry.name for entry in data if not entry.exit_interview])
fnf_pending = len([entry.name for entry in data if not entry.full_and_final_statement])
questionnaires_pending = len([entry.name for entry in data if not entry.questionnaire])
return [
{
'value': total_resignations,
'label': _('Total Resignations'),
'indicator': 'Red' if total_resignations > 0 else 'Green',
'datatype': 'Int',
},
{
'value': interviews_pending,
'label': _('Pending Interviews'),
'indicator': 'Blue' if interviews_pending > 0 else 'Green',
'datatype': 'Int',
},
{
'value': fnf_pending,
'label': _('Pending FnF'),
'indicator': 'Blue' if fnf_pending > 0 else 'Green',
'datatype': 'Int',
},
{
'value': questionnaires_pending,
'label': _('Pending Questionnaires'),
'indicator': 'Blue' if questionnaires_pending > 0 else 'Green',
'datatype': 'Int'
},
]

View File

@ -0,0 +1,242 @@
import unittest
import frappe
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.exit_interview.test_exit_interview import create_exit_interview
from erpnext.hr.doctype.full_and_final_statement.test_full_and_final_statement import (
create_full_and_final_statement,
)
from erpnext.hr.report.employee_exits.employee_exits import execute
class TestEmployeeExits(unittest.TestCase):
@classmethod
def setUpClass(cls):
create_company()
frappe.db.sql("delete from `tabEmployee` where company='Test Company'")
frappe.db.sql("delete from `tabFull and Final Statement` where company='Test Company'")
frappe.db.sql("delete from `tabExit Interview` where company='Test Company'")
cls.create_records()
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
@classmethod
def create_records(cls):
cls.emp1 = make_employee(
'employeeexit1@example.com',
company='Test Company',
date_of_joining=getdate('01-10-2021'),
relieving_date=add_days(getdate(), 14),
designation='Accountant'
)
cls.emp2 = make_employee(
'employeeexit2@example.com',
company='Test Company',
date_of_joining=getdate('01-12-2021'),
relieving_date=add_days(getdate(), 15),
designation='Accountant'
)
cls.emp3 = make_employee(
'employeeexit3@example.com',
company='Test Company',
date_of_joining=getdate('02-12-2021'),
relieving_date=add_days(getdate(), 29),
designation='Engineer'
)
cls.emp4 = make_employee(
'employeeexit4@example.com',
company='Test Company',
date_of_joining=getdate('01-12-2021'),
relieving_date=add_days(getdate(), 30),
designation='Engineer'
)
# exit interview for 3 employees only
cls.interview1 = create_exit_interview(cls.emp1)
cls.interview2 = create_exit_interview(cls.emp2)
cls.interview3 = create_exit_interview(cls.emp3)
# create fnf for some records
cls.fnf1 = create_full_and_final_statement(cls.emp1)
cls.fnf2 = create_full_and_final_statement(cls.emp2)
# link questionnaire for a few records
# setting employee doctype as reference instead of creating a questionnaire
# since this is just for a test
frappe.db.set_value('Exit Interview', cls.interview1.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp1
})
frappe.db.set_value('Exit Interview', cls.interview2.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp2
})
frappe.db.set_value('Exit Interview', cls.interview3.name, {
'ref_doctype': 'Employee',
'reference_document_name': cls.emp3
})
def test_employee_exits_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 15),
'designation': 'Accountant'
}
report = execute(filters)
employee1 = frappe.get_doc('Employee', self.emp1)
employee2 = frappe.get_doc('Employee', self.emp2)
expected_data = [
{
'employee': employee1.name,
'employee_name': employee1.employee_name,
'date_of_joining': employee1.date_of_joining,
'relieving_date': employee1.relieving_date,
'department': employee1.department,
'designation': employee1.designation,
'reports_to': None,
'exit_interview': self.interview1.name,
'interview_status': self.interview1.status,
'employee_status': '',
'questionnaire': employee1.name,
'full_and_final_statement': self.fnf1.name
},
{
'employee': employee2.name,
'employee_name': employee2.employee_name,
'date_of_joining': employee2.date_of_joining,
'relieving_date': employee2.relieving_date,
'department': employee2.department,
'designation': employee2.designation,
'reports_to': None,
'exit_interview': self.interview2.name,
'interview_status': self.interview2.status,
'employee_status': '',
'questionnaire': employee2.name,
'full_and_final_statement': self.fnf2.name
}
]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_exit_interviews_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 30),
'exit_interview_pending': 1
}
report = execute(filters)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_exit_questionnaire_summary(self):
filters = {
'company': 'Test Company',
'from_date': getdate(),
'to_date': add_days(getdate(), 30),
'questionnaire_pending': 1
}
report = execute(filters)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}]
self.assertEqual(expected_data, report[1]) # rows
def test_pending_fnf_summary(self):
filters = {
'company': 'Test Company',
'fnf_pending': 1
}
report = execute(filters)
employee3 = frappe.get_doc('Employee', self.emp3)
employee4 = frappe.get_doc('Employee', self.emp4)
expected_data = [
{
'employee': employee3.name,
'employee_name': employee3.employee_name,
'date_of_joining': employee3.date_of_joining,
'relieving_date': employee3.relieving_date,
'department': employee3.department,
'designation': employee3.designation,
'reports_to': None,
'exit_interview': self.interview3.name,
'interview_status': self.interview3.status,
'employee_status': '',
'questionnaire': employee3.name,
'full_and_final_statement': None
},
{
'employee': employee4.name,
'employee_name': employee4.employee_name,
'date_of_joining': employee4.date_of_joining,
'relieving_date': employee4.relieving_date,
'department': employee4.department,
'designation': employee4.designation,
'reports_to': None,
'exit_interview': None,
'interview_status': None,
'employee_status': None,
'questionnaire': None,
'full_and_final_statement': None
}
]
self.assertEqual(expected_data, report[1]) # rows
def create_company():
if not frappe.db.exists('Company', 'Test Company'):
frappe.get_doc({
'doctype': 'Company',
'company_name': 'Test Company',
'default_currency': 'INR',
'country': 'India'
}).insert()

View File

@ -5,7 +5,7 @@
"label": "Outgoing Salary"
}
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"content": "[{\"type\":\"onboarding\",\"data\":{\"onboarding_name\":\"Human Resource\",\"col\":12}},{\"type\":\"chart\",\"data\":{\"chart_name\":\"Outgoing Salary\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Employee\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leave Application\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Attendance\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Job Applicant\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Monthly Attendance Sheet\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports &amp; Masters\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Lifecycle\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Employee Exit\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Shift Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Leaves\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Attendance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Expense Claims\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Loans\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Recruitment\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Performance\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Fleet Management\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Training\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Key Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Other Reports\",\"col\":4}}]",
"creation": "2020-03-02 15:48:58.322521",
"docstatus": 0,
"doctype": "Workspace",
@ -15,14 +15,6 @@
"idx": 0,
"label": "HR",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -111,14 +103,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Lifecycle",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Job Applicant",
"hidden": 0,
@ -227,14 +211,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Shift Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -268,14 +244,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Leaves",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -386,14 +354,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -449,14 +409,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
@ -489,14 +441,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -530,14 +474,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Fleet Management",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
@ -581,14 +517,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Recruitment",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
@ -808,14 +736,6 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Attendance",
"hidden": 0,
@ -933,9 +853,796 @@
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Lifecycle",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Job Applicant",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Onboarding",
"link_count": 0,
"link_to": "Employee Onboarding",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Skill Map",
"link_count": 0,
"link_to": "Employee Skill Map",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Promotion",
"link_count": 0,
"link_to": "Employee Promotion",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Transfer",
"link_count": 0,
"link_to": "Employee Transfer",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Grievance Type",
"link_count": 0,
"link_to": "Grievance Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grievance",
"link_count": 0,
"link_to": "Employee Grievance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Onboarding Template",
"link_count": 0,
"link_to": "Employee Onboarding Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Exit",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Separation Template",
"link_count": 0,
"link_to": "Employee Separation Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Separation",
"link_count": 0,
"link_to": "Employee Separation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Full and Final Statement",
"link_count": 0,
"link_to": "Full and Final Statement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Exit Interview",
"link_count": 0,
"link_to": "Exit Interview",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 8,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee",
"link_count": 0,
"link_to": "Employee",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employment Type",
"link_count": 0,
"link_to": "Employment Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Branch",
"link_count": 0,
"link_to": "Branch",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Department",
"link_count": 0,
"link_to": "Department",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Designation",
"link_count": 0,
"link_to": "Designation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Grade",
"link_count": 0,
"link_to": "Employee Grade",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Group",
"link_count": 0,
"link_to": "Employee Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Health Insurance",
"link_count": 0,
"link_to": "Employee Health Insurance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Key Reports",
"link_count": 7,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Attendance",
"hidden": 0,
"is_query_report": 1,
"label": "Monthly Attendance Sheet",
"link_count": 0,
"link_to": "Monthly Attendance Sheet",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Staffing Plan",
"hidden": 0,
"is_query_report": 1,
"label": "Recruitment Analytics",
"link_count": 0,
"link_to": "Recruitment Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Analytics",
"link_count": 0,
"link_to": "Employee Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Leave Balance",
"link_count": 0,
"link_to": "Employee Leave Balance",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Leave Balance Summary",
"link_count": 0,
"link_to": "Employee Leave Balance Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee Advance",
"hidden": 0,
"is_query_report": 1,
"label": "Employee Advance Summary",
"link_count": 0,
"link_to": "Employee Advance Summary",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Exits",
"link_count": 0,
"link_to": "Employee Exits",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Recruitment",
"link_count": 11,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Opening",
"link_count": 0,
"link_to": "Job Opening",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Employee Referral",
"link_count": 0,
"link_to": "Employee Referral",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Applicant",
"link_count": 0,
"link_to": "Job Applicant",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Job Offer",
"link_count": 0,
"link_to": "Job Offer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Staffing Plan",
"link_count": 0,
"link_to": "Staffing Plan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Appointment Letter",
"link_count": 0,
"link_to": "Appointment Letter",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Appointment Letter Template",
"link_count": 0,
"link_to": "Appointment Letter Template",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Type",
"link_count": 0,
"link_to": "Interview Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Round",
"link_count": 0,
"link_to": "Interview Round",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview",
"link_count": 0,
"link_to": "Interview",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Interview Feedback",
"link_count": 0,
"link_to": "Interview Feedback",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Fleet Management",
"link_count": 4,
"onboard": 0,
"type": "Card Break"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Driver",
"link_count": 0,
"link_to": "Driver",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Vehicle",
"link_count": 0,
"link_to": "Vehicle",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Vehicle Log",
"link_count": 0,
"link_to": "Vehicle Log",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Vehicle",
"hidden": 0,
"is_query_report": 1,
"label": "Vehicle Expenses",
"link_count": 0,
"link_to": "Vehicle Expenses",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Settings",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "HR Settings",
"link_count": 0,
"link_to": "HR Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Daily Work Summary Group",
"link_count": 0,
"link_to": "Daily Work Summary Group",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Team Updates",
"link_count": 0,
"link_to": "team-updates",
"link_type": "Page",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claims",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Expense Claim",
"link_count": 0,
"link_to": "Expense Claim",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Advance",
"link_count": 0,
"link_to": "Employee Advance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Travel Request",
"link_count": 0,
"link_to": "Travel Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 5,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Attendance Tool",
"link_count": 0,
"link_to": "Employee Attendance Tool",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Attendance",
"link_count": 0,
"link_to": "Attendance",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Attendance Request",
"link_count": 0,
"link_to": "Attendance Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Upload Attendance",
"link_count": 0,
"link_to": "Upload Attendance",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Employee Checkin",
"link_count": 0,
"link_to": "Employee Checkin",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Leaves",
"link_count": 10,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Holiday List",
"link_count": 0,
"link_to": "Holiday List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Type",
"link_count": 0,
"link_to": "Leave Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Period",
"link_count": 0,
"link_to": "Leave Period",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Leave Type",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Policy",
"link_count": 0,
"link_to": "Leave Policy",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Leave Policy",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Policy Assignment",
"link_count": 0,
"link_to": "Leave Policy Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Application",
"link_count": 0,
"link_to": "Leave Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Allocation",
"link_count": 0,
"link_to": "Leave Allocation",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Encashment",
"link_count": 0,
"link_to": "Leave Encashment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Leave Block List",
"link_count": 0,
"link_to": "Leave Block List",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "Employee",
"hidden": 0,
"is_query_report": 0,
"label": "Compensatory Leave Request",
"link_count": 0,
"link_to": "Compensatory Leave Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Shift Management",
"link_count": 3,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Type",
"link_count": 0,
"link_to": "Shift Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Request",
"link_count": 0,
"link_to": "Shift Request",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Shift Assignment",
"link_count": 0,
"link_to": "Shift Assignment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
}
],
"modified": "2021-08-31 12:18:59.842919",
"modified": "2021-12-05 22:05:13.004462",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",

View File

@ -323,9 +323,13 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
target.maintenance_schedule = source.name
target.maintenance_schedule_detail = s_id
def update_sales(source, target, parent):
def update_sales_and_serial(source, target, parent):
sales_person = frappe.db.get_value('Maintenance Schedule Detail', s_id, 'sales_person')
target.service_person = sales_person
serial_nos = get_serial_nos(target.serial_no)
if len(serial_nos) == 1:
target.serial_no = serial_nos[0]
else:
target.serial_no = ''
doclist = get_mapped_doc("Maintenance Schedule", source_name, {
@ -342,7 +346,7 @@ def make_maintenance_visit(source_name, target_doc=None, item_name=None, s_id=No
"Maintenance Schedule Item": {
"doctype": "Maintenance Visit Purpose",
"condition": lambda doc: doc.item_name == item_name,
"postprocess": update_sales
"postprocess": update_sales_and_serial
}
}, target_doc)

View File

@ -43,14 +43,11 @@ frappe.ui.form.on('Maintenance Visit', {
}
});
}
else {
frm.clear_table("purposes");
}
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
frm.clear_table("purposes");
frm.set_value({ mntc_date: frappe.datetime.get_today() });
}
},

View File

@ -75,6 +75,15 @@ frappe.ui.form.on('Job Card', {
&& (frm.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
frm.trigger("prepare_timer_buttons");
}
if (frm.doc.work_order) {
frappe.db.get_value('Work Order', frm.doc.work_order,
'transfer_material_against').then((r) => {
if (r.message.transfer_material_against == 'Work Order') {
frm.set_df_property('items', 'hidden', 1);
}
});
}
},
setup_corrective_job_card: function(frm) {

View File

@ -4,10 +4,17 @@ from frappe import _
def get_data():
return {
'fieldname': 'job_card',
'non_standard_fieldnames': {
'Quality Inspection': 'reference_name'
},
'transactions': [
{
'label': _('Transactions'),
'items': ['Material Request', 'Stock Entry']
},
{
'label': _('Reference'),
'items': ['Quality Inspection']
}
]
}

View File

@ -21,13 +21,17 @@ from erpnext.stock.doctype.item.test_item import create_item, make_item
from erpnext.stock.doctype.stock_entry import test_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.utils import get_bin
from erpnext.tests.utils import ERPNextTestCase, timeout
class TestWorkOrder(unittest.TestCase):
class TestWorkOrder(ERPNextTestCase):
def setUp(self):
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
def tearDown(self):
frappe.db.rollback()
def check_planned_qty(self):
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
@ -376,6 +380,7 @@ class TestWorkOrder(unittest.TestCase):
self.assertEqual(len(ste.additional_costs), 1)
self.assertEqual(ste.total_additional_costs, 1000)
@timeout(seconds=60)
def test_job_card(self):
stock_entries = []
bom = frappe.get_doc('BOM', {
@ -769,6 +774,7 @@ class TestWorkOrder(unittest.TestCase):
total_pl_qty
)
@timeout(seconds=60)
def test_job_card_scrap_item(self):
items = ['Test FG Item for Scrap Item Test', 'Test RM Item 1 for Scrap Item Test',
'Test RM Item 2 for Scrap Item Test']

View File

@ -165,6 +165,7 @@ erpnext.patches.v12_0.set_updated_purpose_in_pick_list
erpnext.patches.v12_0.set_default_payroll_based_on
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
erpnext.patches.v12_0.update_end_date_and_status_in_email_campaign
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.move_tax_slabs_from_payroll_period_to_income_tax_slab #123
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.rename_pos_closing_doctype
@ -278,6 +279,7 @@ erpnext.patches.v13_0.update_tds_check_field #3
erpnext.patches.v13_0.add_custom_field_for_south_africa #2
erpnext.patches.v13_0.update_recipient_email_digest
erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.remove_bad_selling_defaults
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning
@ -286,7 +288,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego
erpnext.patches.v14_0.delete_einvoicing_doctypes
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_gst_payment_entry_fields #27-11-2021
erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.fix_invoice_statuses
@ -312,3 +313,8 @@ erpnext.patches.v13_0.update_category_in_ltds_certificate
erpnext.patches.v13_0.create_pan_field_for_india #2
erpnext.patches.v14_0.delete_hub_doctypes
erpnext.patches.v13_0.create_ksa_vat_custom_fields
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
erpnext.patches.v14_0.migrate_crm_settings
erpnext.patches.v13_0.rename_ksa_qr_field
erpnext.patches.v13_0.disable_ksa_print_format_for_others
erpnext.patches.v14_0.add_default_exit_questionnaire_notification_template

View File

@ -3,9 +3,9 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
import erpnext
from erpnext.regional.india.setup import setup
def execute():
@ -30,7 +30,14 @@ def execute():
frappe.reload_doc('Regional', 'Report', report)
if erpnext.get_region() == "India":
setup(patch=True)
create_custom_field('Salary Component',
dict(fieldname='component_type',
label='Component Type',
fieldtype='Select',
insert_after='description',
options='\nProvident Fund\nAdditional Provident Fund\nProvident Fund Loan\nProfessional Tax',
depends_on='eval:doc.type == "Deduction"')
)
if frappe.db.exists("Salary Component", "Income Tax"):
frappe.db.set_value("Salary Component", "Income Tax", "is_income_tax_component", 1)

View File

@ -0,0 +1,16 @@
# Copyright (c) 2020, Wahni Green Technologies and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if company:
return
if frappe.db.exists('DocType', 'Print Format'):
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in ('KSA VAT Invoice', 'KSA POS Invoice'):
frappe.db.set_value("Print Format", d, "disabled", 1)

View File

@ -0,0 +1,15 @@
import frappe
from frappe import _
def execute():
selling_settings = frappe.get_single("Selling Settings")
if selling_settings.customer_group in (_("All Customer Groups"), "All Customer Groups"):
selling_settings.customer_group = None
if selling_settings.territory in (_("All Territories"), "All Territories"):
selling_settings.territory = None
selling_settings.flags.ignore_mandatory=True
selling_settings.save(ignore_permissions=True)

View File

@ -0,0 +1,32 @@
# Copyright (c) 2020, Wahni Green Technologies and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.utils.rename_field import rename_field
def execute():
company = frappe.get_all('Company', filters = {'country': 'Saudi Arabia'})
if not company:
return
if frappe.db.exists('DocType', 'Sales Invoice'):
frappe.reload_doc('accounts', 'doctype', 'sales_invoice', force=True)
# rename_field method assumes that the field already exists or the doc is synced
if not frappe.db.has_column('Sales Invoice', 'ksa_einv_qr'):
create_custom_fields({
'Sales Invoice': [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
]
})
if frappe.db.has_column('Sales Invoice', 'qr_code'):
rename_field('Sales Invoice', 'qr_code', 'ksa_einv_qr')
frappe.delete_doc_if_exists("Custom Field", "Sales Invoice-qr_code")

View File

@ -0,0 +1,27 @@
import os
import frappe
from frappe import _
def execute():
frappe.reload_doc("email", "doctype", "email_template")
frappe.reload_doc("hr", "doctype", "hr_settings")
template = frappe.db.exists("Email Template", _("Exit Questionnaire Notification"))
if not template:
base_path = frappe.get_app_path("erpnext", "hr", "doctype")
response = frappe.read_file(os.path.join(base_path, "exit_interview/exit_questionnaire_notification_template.html"))
template = frappe.get_doc({
"doctype": "Email Template",
"name": _("Exit Questionnaire Notification"),
"response": response,
"subject": _("Exit Questionnaire Notification"),
"owner": frappe.session.user,
}).insert(ignore_permissions=True)
template = template.name
hr_settings = frappe.get_doc("HR Settings")
hr_settings.exit_questionnaire_notification_template = template
hr_settings.save()

View File

@ -0,0 +1,16 @@
import frappe
def execute():
settings = frappe.db.get_value('Selling Settings', 'Selling Settings', [
'campaign_naming_by',
'close_opportunity_after_days',
'default_valid_till'
], as_dict=True)
frappe.reload_doc('crm', 'doctype', 'crm_settings')
frappe.db.set_value('CRM Settings', 'CRM Settings', {
'campaign_naming_by': settings.campaign_naming_by,
'close_opportunity_after_days': settings.close_opportunity_after_days,
'default_valid_till': settings.default_valid_till
})

View File

@ -0,0 +1,27 @@
import frappe
def execute():
active_sla_documents = [sla.document_type for sla in frappe.get_all("Service Level Agreement", fields=["document_type"])]
for doctype in active_sla_documents:
doctype = frappe.qb.DocType(doctype)
try:
frappe.qb.update(
doctype
).set(
doctype.agreement_status, 'First Response Due'
).where(
doctype.first_responded_on.isnull()
).run()
frappe.qb.update(
doctype
).set(
doctype.agreement_status, 'Resolution Due'
).where(
doctype.agreement_status == 'Ongoing'
).run()
except Exception:
frappe.log_error(title='Failed to Patch SLA Status')

View File

@ -940,10 +940,12 @@ class SalarySlip(TransactionBase):
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount
timesheet_component = frappe.db.get_value("Salary Structure", self.salary_structure, "salary_component")
if (self.salary_structure and
cint(row.depends_on_payment_days) and cint(self.total_working_days)
and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
and (not self.salary_slip_based_on_timesheet or
and (row.salary_component != timesheet_component or
getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date)
)):
@ -952,7 +954,7 @@ class SalarySlip(TransactionBase):
amount = flt((flt(row.default_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
elif not self.payment_days and not self.salary_slip_based_on_timesheet and cint(row.depends_on_payment_days):
elif not self.payment_days and row.salary_component != timesheet_component and cint(row.depends_on_payment_days):
amount, additional_amount = 0, 0
elif not row.amount:
amount = flt(row.default_amount) + flt(row.additional_amount)

View File

@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase):
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
def test_payment_days_in_salary_slip_based_on_timesheet(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.projects.doctype.timesheet.test_timesheet import (
make_salary_structure_for_timesheet,
make_timesheet,
)
from erpnext.projects.doctype.timesheet.timesheet import (
make_salary_slip as make_salary_slip_for_timesheet,
)
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
emp = make_employee("test_employee_timesheet@salary.com", company="_Test Company")
frappe.db.set_value("Employee", emp, {"relieving_date": None, "status": "Active"})
# mark attendance
month_start_date = get_first_day(nowdate())
month_end_date = get_last_day(nowdate())
first_sunday = frappe.db.sql("""
select holiday_date from `tabHoliday`
where parent = 'Salary Slip Test Holiday List'
and holiday_date between %s and %s
order by holiday_date
""", (month_start_date, month_end_date))[0][0]
mark_attendance(emp, add_days(first_sunday, 1), 'Absent', ignore_validate=True) # counted as absent
# salary structure based on timesheet
make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
salary_slip = make_salary_slip_for_timesheet(timesheet.name)
salary_slip.start_date = month_start_date
salary_slip.end_date = month_end_date
salary_slip.save()
salary_slip.submit()
no_of_days = self.get_no_of_days()
days_in_month = no_of_days[0]
no_of_holidays = no_of_days[1]
self.assertEqual(salary_slip.payment_days, days_in_month - no_of_holidays - 1)
# gross pay calculation based on attendance (payment days)
gross_pay = 78100 - ((78000 / (days_in_month - no_of_holidays)) * flt(salary_slip.leave_without_pay + salary_slip.absent_days))
self.assertEqual(salary_slip.gross_pay, flt(gross_pay, 2))
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
def test_component_amount_dependent_on_another_payment_days_based_component(self):
from erpnext.hr.doctype.attendance.attendance import mark_attendance
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (

View File

@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase):
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
frappe.db.sql("delete from `tab%s`" % dt)
if not frappe.db.exists("Salary Component", "Timesheet Component"):
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
def test_timesheet_billing_amount(self):
emp = make_employee("test_employee_6@salary.com")
@ -160,6 +156,9 @@ def make_salary_structure_for_timesheet(employee, company=None):
salary_structure_name = "Timesheet Salary Structure Test"
frequency = "Monthly"
if not frappe.db.exists("Salary Component", "Timesheet Component"):
frappe.get_doc({"doctype": "Salary Component", "salary_component": "Timesheet Component"}).insert()
salary_structure = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
salary_structure.salary_component = "Timesheet Component"
salary_structure.salary_slip_based_on_timesheet = 1

View File

@ -1,7 +1,7 @@
import unittest
import frappe
from frappe.utils import add_days, getdate, nowdate
from frappe.utils import add_days, getdate
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.projects.doctype.timesheet.test_timesheet import (
@ -13,21 +13,26 @@ from erpnext.projects.report.project_profitability.project_profitability import
class TestProjectProfitability(unittest.TestCase):
def setUp(self):
frappe.db.sql('delete from `tabTimesheet`')
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
make_salary_structure_for_timesheet(emp, company='_Test Company')
self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
date = getdate()
self.timesheet = make_timesheet(emp, is_billable=1)
self.salary_slip = make_salary_slip(self.timesheet.name)
holidays = self.salary_slip.get_holidays_for_employee(nowdate(), nowdate())
holidays = self.salary_slip.get_holidays_for_employee(date, date)
if holidays:
frappe.db.set_value('Payroll Settings', None, 'include_holidays_in_total_working_days', 1)
self.salary_slip.submit()
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
self.sales_invoice.due_date = nowdate()
self.sales_invoice.due_date = date
self.sales_invoice.submit()
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
@ -63,6 +68,4 @@ class TestProjectProfitability(unittest.TestCase):
self.assertEqual(fractional_cost, row.fractional_cost)
def tearDown(self):
frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
frappe.get_doc("Timesheet", self.timesheet.name).cancel()
frappe.db.rollback()

View File

@ -1106,7 +1106,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
$.each(this.frm.doc.taxes || [], function(i, d) {
if(d.charge_type == "Actual") {
frappe.model.set_value(d.doctype, d.name, "tax_amount",
flt(d.tax_amount) / flt(exchange_rate));
flt(d.base_tax_amount) / flt(exchange_rate));
}
});
}

View File

@ -84,6 +84,10 @@ $.extend(erpnext, {
});
},
route_to_pending_reposts: (args) => {
frappe.set_route('List', 'Repost Item Valuation', args);
},
proceed_save_with_reminders_frequency_change: () => {
frappe.ui.hide_open_dialog();
@ -831,7 +835,7 @@ $(document).on('app_ready', function() {
refresh: function(frm) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
&& frm.doc.agreement_status === 'Ongoing') {
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
frappe.call({
'method': 'frappe.client.get',
args: {
@ -884,8 +888,8 @@ $(document).on('app_ready', function() {
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
frm.dashboard.clear_headline();
let time_to_respond = get_status(frm.doc.response_by_variance);
if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
let time_to_respond = get_status(frm.doc.response_by);
if (!frm.doc.first_responded_on) {
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
}
@ -899,8 +903,8 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
if (apply_sla_for_resolution) {
let time_to_resolve = get_status(frm.doc.resolution_by_variance);
if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
let time_to_resolve = get_status(frm.doc.resolution_by);
if (!frm.doc.resolution_date) {
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
}
@ -924,8 +928,9 @@ function get_time_left(timestamp, agreement_status) {
return {'diff_display': diff_display, 'indicator': indicator};
}
function get_status(variance) {
if (variance > 0) {
function get_status(timestamp) {
const time_left = moment(timestamp).diff(moment());
if (time_left >= 0) {
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
} else {
return {'diff_display': 'Failed', 'indicator': 'red'};

View File

@ -82,7 +82,6 @@ class TaxExemption80GCertificate(Document):
memberships = frappe.db.get_all('Membership', {
'member': self.member,
'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
'membership_status': ('!=', 'Cancelled')
}, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')

View File

@ -206,26 +206,17 @@ def get_regional_address_details(party_details, doctype, company):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
return party_details
if not party_details.company_gstin:
return party_details
tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_based_on_category(master_doctype, company, party_details)
tax_template_by_category = get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
return party_details
if not party_details.supplier_gstin:
return party_details
if tax_template_by_category:
party_details['taxes_and_charges'] = tax_template_by_category
return
if not party_details.place_of_supply: return party_details
if not party_details.company_gstin: return party_details
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
@ -237,6 +228,7 @@ def get_regional_address_details(party_details, doctype, company):
if not default_tax:
return party_details
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
@ -268,9 +260,7 @@ def get_tax_template_based_on_category(master_doctype, company, party_details):
default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')},
'name')
if default_tax:
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
return default_tax
def get_tax_template(master_doctype, company, is_inter_state, state_code):
tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'],
@ -847,13 +837,11 @@ def update_taxable_values(doc, method):
doc.get('items')[item_count - 1].taxable_value += diff
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"):
# if the Depreciation Schedule is being prepared for the first time
if not asset.flags.increase_in_asset_life:
depreciation_amount = (flt(asset.gross_purchase_amount) - flt(asset.opening_accumulated_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
depreciation_amount = (flt(asset.gross_purchase_amount) -
flt(row.expected_value_after_useful_life)) / flt(row.total_number_of_depreciations)
# if the Depreciation Schedule is being modified after Asset Repair
else:

View File

@ -0,0 +1,32 @@
{
"absolute_value": 0,
"align_labels_right": 0,
"creation": "2021-12-07 13:25:05.424827",
"css": "",
"custom_format": 1,
"default_print_language": "en",
"disabled": 1,
"doc_type": "POS Invoice",
"docstatus": 0,
"doctype": "Print Format",
"font_size": 0,
"html": "<style>\n\t.print-format table, .print-format tr, \n\t.print-format td, .print-format div, .print-format p {\n\t\tline-height: 150%;\n\t\tvertical-align: middle;\n\t}\n\t@media screen {\n\t\t.print-format {\n\t\t\twidth: 4in;\n\t\t\tpadding: 0.25in;\n\t\t\tmin-height: 8in;\n\t\t}\n\t}\n</style>\n\n{% if letter_head %}\n {{ letter_head }}\n{% endif %}\n\n<p class=\"text-center\" style=\"margin-bottom: 1rem\">\n\t{{ doc.company }}<br>\n\t<b>{{ doc.select_print_heading or _(\"Invoice\") }}</b><br>\n\t<img src={{doc.ksa_einv_qr}}>\n</p>\n<p>\n\t<b>{{ _(\"Receipt No\") }}:</b> {{ doc.name }}<br>\n\t<b>{{ _(\"Cashier\") }}:</b> {{ doc.owner }}<br>\n\t<b>{{ _(\"Customer\") }}:</b> {{ doc.customer_name }}<br>\n\t<b>{{ _(\"Date\") }}:</b> {{ doc.get_formatted(\"posting_date\") }}<br>\n\t<b>{{ _(\"Time\") }}:</b> {{ doc.get_formatted(\"posting_time\") }}<br>\n</p>\n\n<hr>\n<table class=\"table table-condensed\">\n\t<thead>\n\t\t<tr>\n\t\t\t<th width=\"40%\">{{ _(\"Item\") }}</th>\n\t\t\t<th width=\"25%\" class=\"text-right\">{{ _(\"Qty\") }}</th>\n\t\t\t<th width=\"35%\" class=\"text-right\">{{ _(\"Amount\") }}</th>\n\t\t</tr>\n\t</thead>\n\t<tbody>\n\t\t{%- for item in doc.items -%}\n\t\t<tr>\n\t\t\t<td>\n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t<br>{{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t<br><b>{{ _(\"SR.No\") }}:</b><br>\n\t\t\t\t\t{{ item.serial_no | replace(\"\\n\", \", \") }}\n\t\t\t\t{%- endif -%}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">{{ item.qty }}</td>\n\t\t\t<td class=\"text-right\">{{ item.get_formatted(\"net_amount\") }}</td>\n\t\t</tr>\n\t\t{%- endfor -%}\n\t</tbody>\n</table>\n<table class=\"table table-condensed no-border\">\n\t<tbody>\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- for row in doc.taxes -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t {% if '%' in row.description %}\n\t\t\t\t\t {{ row.description }}\n\t\t\t\t\t{% else %}\n\t\t\t\t\t {{ row.description }}@{{ row.rate }}%\n\t\t\t\t\t{% endif %}\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t</td>\n\t\t\t<tr>\n\t\t{%- endfor -%}\n\n\t\t{%- if doc.discount_amount -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Grand Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.rounded_total -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Rounded Total\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- endif -%}\n\t\t<tr>\n\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t<b>{{ _(\"Paid Amount\") }}</b>\n\t\t\t</td>\n\t\t\t<td class=\"text-right\">\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t</td>\n\t\t</tr>\n\t\t{%- if doc.change_amount -%}\n\t\t\t<tr>\n\t\t\t\t<td class=\"text-right\" style=\"width: 60%\">\n\t\t\t\t\t<b>{{ _(\"Change Amount\") }}</b>\n\t\t\t\t</td>\n\t\t\t\t<td class=\"text-right\">\n\t\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t\t</td>\n\t\t\t</tr>\n\t\t{%- endif -%}\n\t</tbody>\n</table>\n<hr>\n<p>{{ doc.terms or \"\" }}</p>\n<p class=\"text-center\">{{ _(\"Thank you, please visit again.\") }}</p>",
"idx": 0,
"line_breaks": 0,
"margin_bottom": 0.0,
"margin_left": 0.0,
"margin_right": 0.0,
"margin_top": 0.0,
"modified": "2021-12-08 10:25:01.930885",
"modified_by": "Administrator",
"module": "Regional",
"name": "KSA POS Invoice",
"owner": "Administrator",
"page_number": "Hide",
"print_format_builder": 0,
"print_format_builder_beta": 0,
"print_format_type": "Jinja",
"raw_printing": 0,
"show_section_headings": 0,
"standard": "Yes"
}

File diff suppressed because one or more lines are too long

View File

@ -114,9 +114,11 @@ def get_items(filters):
items = frappe.db.sql("""
select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
`tabSales Invoice Item`.gst_hsn_code,
`tabSales Invoice Item`.stock_uom,
sum(`tabSales Invoice Item`.stock_qty) as stock_qty,
sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount,
sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate,
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
`tabGST HSN Code`.description
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
@ -124,6 +126,8 @@ def get_items(filters):
and `tabSales Invoice`.docstatus = 1
and `tabSales Invoice Item`.gst_hsn_code is not NULL
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
group by
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code
""" % (conditions, match_conditions), filters, as_dict=1)

View File

@ -0,0 +1,89 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from unittest import TestCase
import frappe
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
make_company as setup_company,
)
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
make_customers as setup_customers,
)
from erpnext.regional.doctype.gstr_3b_report.test_gstr_3b_report import (
set_account_heads as setup_gst_settings,
)
from erpnext.regional.report.hsn_wise_summary_of_outward_supplies.hsn_wise_summary_of_outward_supplies import (
execute as run_report,
)
from erpnext.stock.doctype.item.test_item import make_item
class TestHSNWiseSummaryReport(TestCase):
@classmethod
def setUpClass(cls):
setup_company()
setup_customers()
setup_gst_settings()
make_item("Golf Car", properties={ "gst_hsn_code": "999900" })
@classmethod
def tearDownClass(cls):
frappe.db.rollback()
def test_hsn_summary_for_invoice_with_duplicate_items(self):
si = create_sales_invoice(
company="_Test Company GST",
customer = "_Test GST Customer",
currency = "INR",
warehouse = "Finished Goods - _GST",
debit_to = "Debtors - _GST",
income_account = "Sales - _GST",
expense_account = "Cost of Goods Sold - _GST",
cost_center = "Main - _GST",
do_not_save=1
)
si.items = []
si.append("items", {
"item_code": "Golf Car",
"gst_hsn_code": "999900",
"qty": "1",
"rate": "120",
"cost_center": "Main - _GST"
})
si.append("items", {
"item_code": "Golf Car",
"gst_hsn_code": "999900",
"qty": "1",
"rate": "140",
"cost_center": "Main - _GST"
})
si.append("taxes", {
"charge_type": "On Net Total",
"account_head": "Output Tax IGST - _GST",
"cost_center": "Main - _GST",
"description": "IGST @ 18.0",
"rate": 18
})
si.posting_date = "2020-11-17"
si.submit()
si.reload()
[columns, data] = run_report(filters=frappe._dict({
"company": "_Test Company GST",
"gst_hsn_code": "999900",
"company_gstin": si.company_gstin,
"from_date": si.posting_date,
"to_date": si.posting_date
}))
filtered_rows = list(filter(lambda row: row['gst_hsn_code'] == "999900", data))
self.assertTrue(filtered_rows)
hsn_row = filtered_rows[0]
self.assertEquals(hsn_row['stock_qty'], 2.0)
self.assertEquals(hsn_row['total_amount'], 306.8)

View File

@ -3,7 +3,7 @@
import frappe
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields
from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
@ -13,6 +13,16 @@ def setup(company=None, patch=True):
add_permissions()
make_custom_fields()
def add_print_formats():
frappe.reload_doc("regional", "print_format", "detailed_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "simplified_tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "tax_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_vat_invoice", force=True)
frappe.reload_doc("regional", "print_format", "ksa_pos_invoice", force=True)
for d in ('Simplified Tax Invoice', 'Detailed Tax Invoice', 'Tax Invoice', 'KSA VAT Invoice', 'KSA POS Invoice'):
frappe.db.set_value("Print Format", d, "disabled", 0)
def add_permissions():
"""Add Permissions for KSA VAT Setting."""
add_permission('KSA VAT Setting', 'All', 0)
@ -33,8 +43,16 @@ def make_custom_fields():
custom_fields = {
'Sales Invoice': [
dict(
fieldname='qr_code',
label='QR Code',
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
],
'POS Invoice': [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)

View File

@ -4,33 +4,39 @@ from base64 import b64encode
import frappe
from frappe import _
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.utils.data import add_to_date, get_time, getdate
from pyqrcode import create as qr_create
from erpnext import get_region
def create_qr_code(doc, method):
"""Create QR Code after inserting Sales Inv
"""
def create_qr_code(doc, method=None):
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
# if QR Code field not present, do nothing
if not hasattr(doc, 'qr_code'):
return
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
if not hasattr(doc, 'ksa_einv_qr'):
create_custom_fields({
doc.doctype: [
dict(
fieldname='ksa_einv_qr',
label='KSA E-Invoicing QR',
fieldtype='Attach Image',
read_only=1, no_copy=1, hidden=1
)
]
})
# Don't create QR Code if it already exists
qr_code = doc.get("qr_code")
qr_code = doc.get("ksa_einv_qr")
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
return
meta = frappe.get_meta('Sales Invoice')
meta = frappe.get_meta(doc.doctype)
for field in meta.get_image_fields():
if field.fieldname == 'qr_code':
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
''' TLV conversion for
1. Seller's Name
2. VAT Number
@ -77,7 +83,7 @@ def create_qr_code(doc, method):
tlv_array.append(''.join([tag, length, value]))
# Invoice Amount
invoice_amount = str(doc.total)
invoice_amount = str(doc.grand_total)
tag = bytes([4]).hex()
length = bytes([len(invoice_amount)]).hex()
value = invoice_amount.encode('utf-8').hex()
@ -101,8 +107,10 @@ def create_qr_code(doc, method):
url = qr_create(base64_string, error='L')
url.png(qr_image, scale=2, quiet_zone=1)
name = frappe.generate_hash(doc.name, 5)
# making file
filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
_file = frappe.get_doc({
"doctype": "File",
"file_name": filename,
@ -110,36 +118,32 @@ def create_qr_code(doc, method):
"content": qr_image.getvalue(),
"attached_to_doctype": doc.get("doctype"),
"attached_to_name": doc.get("name"),
"attached_to_field": "qr_code"
"attached_to_field": "ksa_einv_qr"
})
_file.save()
# assigning to document
doc.db_set('qr_code', _file.file_url)
doc.db_set('ksa_einv_qr', _file.file_url)
doc.notify_update()
break
def delete_qr_code_file(doc, method):
"""Delete QR Code on deleted sales invoice"""
def delete_qr_code_file(doc, method=None):
region = get_region(doc.company)
if region not in ['Saudi Arabia']:
return
if hasattr(doc, 'qr_code'):
if doc.get('qr_code'):
if hasattr(doc, 'ksa_einv_qr'):
if doc.get('ksa_einv_qr'):
file_doc = frappe.get_list('File', {
'file_url': doc.get('qr_code')
'file_url': doc.get('ksa_einv_qr')
})
if len(file_doc):
frappe.delete_doc('File', file_doc[0].name)
def delete_vat_settings_for_company(doc, method):
def delete_vat_settings_for_company(doc, method=None):
if doc.country != 'Saudi Arabia':
return
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
settings_doc.delete()
if frappe.db.exists('KSA VAT Setting', doc.name):
frappe.delete_doc('KSA VAT Setting', doc.name)

View File

@ -961,9 +961,7 @@
"idx": 82,
"is_submittable": 1,
"links": [],
"max_attachments": 1,
"migration_hash": "75a86a19f062c2257bcbc8e6e31c7f1e",
"modified": "2021-10-21 12:58:55.514512",
"modified": "2021-11-30 01:33:21.106073",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation",

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