mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-13 10:11:20 +00:00
Merge branch 'develop' into maint_sch_link_fix
This commit is contained in:
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
47
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -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
106
.github/ISSUE_TEMPLATE/bug_report.yaml
vendored
Normal 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
|
||||||
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
3
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,7 +1,10 @@
|
|||||||
---
|
---
|
||||||
name: Feature request
|
name: Feature request
|
||||||
about: Suggest an idea to improve ERPNext
|
about: Suggest an idea to improve ERPNext
|
||||||
|
title: ''
|
||||||
labels: feature-request
|
labels: feature-request
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
|
|||||||
@@ -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
56
.github/stale.yml
vendored
@@ -1,34 +1,36 @@
|
|||||||
# Configuration for probot-stale - https://github.com/probot/stale
|
# 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
|
# Label to use when marking as stale
|
||||||
staleLabel: inactive
|
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
|
# Limit the number of actions per hour, from 1-30. Default is 30
|
||||||
limitPerRun: 30
|
limitPerRun: 10
|
||||||
|
|
||||||
# Limit to only `issues` or `pulls`
|
# Set to true to ignore issues in a project (defaults to false)
|
||||||
only: pulls
|
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.
|
||||||
|
|||||||
10
codecov.yml
10
codecov.yml
@@ -8,6 +8,16 @@ coverage:
|
|||||||
target: auto
|
target: auto
|
||||||
threshold: 0.5%
|
threshold: 0.5%
|
||||||
|
|
||||||
|
patch:
|
||||||
|
default:
|
||||||
|
target: 85%
|
||||||
|
threshold: 0%
|
||||||
|
base: auto
|
||||||
|
branches:
|
||||||
|
- develop
|
||||||
|
if_ci_failed: ignore
|
||||||
|
only_pulls: true
|
||||||
|
|
||||||
comment:
|
comment:
|
||||||
layout: "diff, files"
|
layout: "diff, files"
|
||||||
require_changes: true
|
require_changes: true
|
||||||
|
|||||||
@@ -374,12 +374,13 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
|||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
if frappe.flags.in_test:
|
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
|
raise e
|
||||||
else:
|
else:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
traceback = frappe.get_traceback()
|
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
|
frappe.flags.deferred_accounting_error = True
|
||||||
|
|
||||||
def send_mail(deferred_process):
|
def send_mail(deferred_process):
|
||||||
@@ -446,10 +447,12 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
|||||||
|
|
||||||
if submit:
|
if submit:
|
||||||
journal_entry.submit()
|
journal_entry.submit()
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
except Exception:
|
except Exception:
|
||||||
frappe.db.rollback()
|
frappe.db.rollback()
|
||||||
traceback = frappe.get_traceback()
|
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
|
frappe.flags.deferred_accounting_error = True
|
||||||
|
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ from frappe.custom.doctype.property_setter.property_setter import make_property_
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.utils import cint
|
from frappe.utils import cint
|
||||||
|
|
||||||
|
from erpnext.stock.utils import check_pending_reposting
|
||||||
|
|
||||||
|
|
||||||
class AccountsSettings(Document):
|
class AccountsSettings(Document):
|
||||||
def on_update(self):
|
def on_update(self):
|
||||||
@@ -25,6 +27,7 @@ class AccountsSettings(Document):
|
|||||||
self.validate_stale_days()
|
self.validate_stale_days()
|
||||||
self.enable_payment_schedule_in_print()
|
self.enable_payment_schedule_in_print()
|
||||||
self.toggle_discount_accounting_fields()
|
self.toggle_discount_accounting_fields()
|
||||||
|
self.validate_pending_reposts()
|
||||||
|
|
||||||
def validate_stale_days(self):
|
def validate_stale_days(self):
|
||||||
if not self.allow_stale and cint(self.stale_days) <= 0:
|
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(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)
|
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)
|
||||||
|
|||||||
@@ -159,7 +159,8 @@ class OpeningInvoiceCreationTool(Document):
|
|||||||
frappe.scrub(row.party_type): row.party,
|
frappe.scrub(row.party_type): row.party,
|
||||||
"is_pos": 0,
|
"is_pos": 0,
|
||||||
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
"doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
|
||||||
"update_stock": 0
|
"update_stock": 0,
|
||||||
|
"invoice_number": row.invoice_number
|
||||||
})
|
})
|
||||||
|
|
||||||
accounting_dimension = get_accounting_dimensions()
|
accounting_dimension = get_accounting_dimensions()
|
||||||
@@ -200,10 +201,13 @@ def start_import(invoices):
|
|||||||
names = []
|
names = []
|
||||||
for idx, d in enumerate(invoices):
|
for idx, d in enumerate(invoices):
|
||||||
try:
|
try:
|
||||||
|
invoice_number = None
|
||||||
|
if d.invoice_number:
|
||||||
|
invoice_number = d.invoice_number
|
||||||
publish(idx, len(invoices), d.doctype)
|
publish(idx, len(invoices), d.doctype)
|
||||||
doc = frappe.get_doc(d)
|
doc = frappe.get_doc(d)
|
||||||
doc.flags.ignore_mandatory = True
|
doc.flags.ignore_mandatory = True
|
||||||
doc.insert()
|
doc.insert(set_name=invoice_number)
|
||||||
doc.submit()
|
doc.submit()
|
||||||
frappe.db.commit()
|
frappe.db.commit()
|
||||||
names.append(doc.name)
|
names.append(doc.name)
|
||||||
|
|||||||
@@ -18,10 +18,10 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
|||||||
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
|
||||||
make_company()
|
make_company()
|
||||||
|
|
||||||
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
|
def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None, invoice_number=None):
|
||||||
doc = frappe.get_single("Opening Invoice Creation Tool")
|
doc = frappe.get_single("Opening Invoice Creation Tool")
|
||||||
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
|
||||||
party_1=party_1, party_2=party_2)
|
party_1=party_1, party_2=party_2, invoice_number=invoice_number)
|
||||||
doc.update(args)
|
doc.update(args)
|
||||||
return doc.make_invoices()
|
return doc.make_invoices()
|
||||||
|
|
||||||
@@ -92,6 +92,20 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
|
|||||||
# teardown
|
# teardown
|
||||||
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
|
||||||
|
|
||||||
|
def test_renaming_of_invoice_using_invoice_number_field(self):
|
||||||
|
company = "_Test Opening Invoice Company"
|
||||||
|
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
|
||||||
|
self.make_invoices(company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11")
|
||||||
|
|
||||||
|
sales_inv1 = frappe.get_all('Sales Invoice', filters={'customer':'Customer A'})[0].get("name")
|
||||||
|
sales_inv2 = frappe.get_all('Sales Invoice', filters={'customer':'Customer B'})[0].get("name")
|
||||||
|
self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
|
||||||
|
|
||||||
|
#teardown
|
||||||
|
for inv in [sales_inv1, sales_inv2]:
|
||||||
|
doc = frappe.get_doc('Sales Invoice', inv)
|
||||||
|
doc.cancel()
|
||||||
|
|
||||||
def get_opening_invoice_creation_dict(**args):
|
def get_opening_invoice_creation_dict(**args):
|
||||||
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
|
||||||
company = args.get("company", "_Test Company")
|
company = args.get("company", "_Test Company")
|
||||||
@@ -107,7 +121,8 @@ def get_opening_invoice_creation_dict(**args):
|
|||||||
"item_name": "Opening Item",
|
"item_name": "Opening Item",
|
||||||
"due_date": "2016-09-10",
|
"due_date": "2016-09-10",
|
||||||
"posting_date": "2016-09-05",
|
"posting_date": "2016-09-05",
|
||||||
"temporary_opening_account": get_temporary_opening_account(company)
|
"temporary_opening_account": get_temporary_opening_account(company),
|
||||||
|
"invoice_number": args.get("invoice_number")
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"qty": 2.0,
|
"qty": 2.0,
|
||||||
@@ -116,7 +131,8 @@ def get_opening_invoice_creation_dict(**args):
|
|||||||
"item_name": "Opening Item",
|
"item_name": "Opening Item",
|
||||||
"due_date": "2016-09-10",
|
"due_date": "2016-09-10",
|
||||||
"posting_date": "2016-09-05",
|
"posting_date": "2016-09-05",
|
||||||
"temporary_opening_account": get_temporary_opening_account(company)
|
"temporary_opening_account": get_temporary_opening_account(company),
|
||||||
|
"invoice_number": None
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"actions": [],
|
||||||
"creation": "2017-08-29 04:26:36.159247",
|
"creation": "2017-08-29 04:26:36.159247",
|
||||||
"doctype": "DocType",
|
"doctype": "DocType",
|
||||||
"editable_grid": 1,
|
"editable_grid": 1,
|
||||||
"engine": "InnoDB",
|
"engine": "InnoDB",
|
||||||
"field_order": [
|
"field_order": [
|
||||||
|
"invoice_number",
|
||||||
"party_type",
|
"party_type",
|
||||||
"party",
|
"party",
|
||||||
"temporary_opening_account",
|
"temporary_opening_account",
|
||||||
@@ -103,10 +105,17 @@
|
|||||||
{
|
{
|
||||||
"fieldname": "dimension_col_break",
|
"fieldname": "dimension_col_break",
|
||||||
"fieldtype": "Column Break"
|
"fieldtype": "Column Break"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Reference number of the invoice from the previous system",
|
||||||
|
"fieldname": "invoice_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Invoice Number"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"modified": "2019-07-25 15:00:00.460695",
|
"links": [],
|
||||||
|
"modified": "2021-12-13 18:15:41.295007",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Opening Invoice Creation Tool Item",
|
"name": "Opening Invoice Creation Tool Item",
|
||||||
|
|||||||
@@ -114,6 +114,9 @@ class PurchaseInvoice(BuyingController):
|
|||||||
self.set_status()
|
self.set_status()
|
||||||
self.validate_purchase_receipt_if_update_stock()
|
self.validate_purchase_receipt_if_update_stock()
|
||||||
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference)
|
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):
|
def validate_release_date(self):
|
||||||
if self.release_date and getdate(nowdate()) >= getdate(self.release_date):
|
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
|
item.expense_account = stock_not_billed_account
|
||||||
|
|
||||||
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category):
|
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)
|
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:
|
elif item.is_fixed_asset and item.pr_detail:
|
||||||
item.expense_account = asset_received_but_not_billed
|
item.expense_account = asset_received_but_not_billed
|
||||||
elif not item.expense_account and for_validate:
|
elif not item.expense_account and for_validate:
|
||||||
|
|||||||
@@ -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:
|
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)
|
validate_loyalty_points(self, self.loyalty_points)
|
||||||
|
|
||||||
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_fixed_asset(self):
|
def validate_fixed_asset(self):
|
||||||
for d in self.get("items"):
|
for d in self.get("items"):
|
||||||
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
if d.is_fixed_asset and d.meta.get_field("asset") and d.asset:
|
||||||
|
|||||||
@@ -75,7 +75,8 @@
|
|||||||
"fieldname": "cost",
|
"fieldname": "cost",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Cost"
|
"label": "Cost",
|
||||||
|
"options": "currency"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
|
"depends_on": "eval:doc.price_determination==\"Based On Price List\"",
|
||||||
@@ -147,7 +148,7 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-08-13 10:53:44.205774",
|
"modified": "2021-12-10 15:24:15.794477",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Accounts",
|
"module": "Accounts",
|
||||||
"name": "Subscription Plan",
|
"name": "Subscription Plan",
|
||||||
|
|||||||
@@ -545,7 +545,9 @@ class ReceivablePayableReport(object):
|
|||||||
|
|
||||||
def set_ageing(self, row):
|
def set_ageing(self, row):
|
||||||
if self.filters.ageing_based_on == "Due Date":
|
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":
|
elif self.filters.ageing_based_on == "Supplier Invoice Date":
|
||||||
entry_date = row.bill_date
|
entry_date = row.bill_date
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -0,0 +1,114 @@
|
|||||||
|
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
// For license information, please see license.txt
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
function get_filters() {
|
||||||
|
let filters = [
|
||||||
|
{
|
||||||
|
"fieldname":"company",
|
||||||
|
"label": __("Company"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"default": frappe.defaults.get_user_default("Company"),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"filter_based_on",
|
||||||
|
"label": __("Filter Based On"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": ["Fiscal Year", "Date Range"],
|
||||||
|
"default": ["Fiscal Year"],
|
||||||
|
"reqd": 1,
|
||||||
|
on_change: function() {
|
||||||
|
let filter_based_on = frappe.query_report.get_filter_value('filter_based_on');
|
||||||
|
frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range');
|
||||||
|
frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range');
|
||||||
|
frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year');
|
||||||
|
frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year');
|
||||||
|
|
||||||
|
frappe.query_report.refresh();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"period_start_date",
|
||||||
|
"label": __("Start Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"hidden": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"period_end_date",
|
||||||
|
"label": __("End Date"),
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"hidden": 1,
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"from_fiscal_year",
|
||||||
|
"label": __("Start Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname":"to_fiscal_year",
|
||||||
|
"label": __("End Year"),
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Fiscal Year",
|
||||||
|
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "periodicity",
|
||||||
|
"label": __("Periodicity"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [
|
||||||
|
{ "value": "Monthly", "label": __("Monthly") },
|
||||||
|
{ "value": "Quarterly", "label": __("Quarterly") },
|
||||||
|
{ "value": "Half-Yearly", "label": __("Half-Yearly") },
|
||||||
|
{ "value": "Yearly", "label": __("Yearly") }
|
||||||
|
],
|
||||||
|
"default": "Monthly",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "type",
|
||||||
|
"label": __("Invoice Type"),
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": [
|
||||||
|
{ "value": "Revenue", "label": __("Revenue") },
|
||||||
|
{ "value": "Expense", "label": __("Expense") }
|
||||||
|
],
|
||||||
|
"default": "Revenue",
|
||||||
|
"reqd": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname" : "with_upcoming_postings",
|
||||||
|
"label": __("Show with upcoming revenue/expense"),
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"default": 1
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
return filters;
|
||||||
|
}
|
||||||
|
|
||||||
|
frappe.query_reports["Deferred Revenue and Expense"] = {
|
||||||
|
"filters": get_filters(),
|
||||||
|
"formatter": function(value, row, column, data, default_formatter){
|
||||||
|
return default_formatter(value, row, column, data);
|
||||||
|
},
|
||||||
|
onload: function(report){
|
||||||
|
let fiscal_year = frappe.defaults.get_user_default("fiscal_year");
|
||||||
|
|
||||||
|
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||||
|
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||||
|
frappe.query_report.set_filter_value({
|
||||||
|
period_start_date: fy.year_start_date,
|
||||||
|
period_end_date: fy.year_end_date
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"add_total_row": 0,
|
||||||
|
"columns": [],
|
||||||
|
"creation": "2021-12-10 19:27:14.654220",
|
||||||
|
"disable_prepared_report": 0,
|
||||||
|
"disabled": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Report",
|
||||||
|
"filters": [],
|
||||||
|
"idx": 0,
|
||||||
|
"is_standard": "Yes",
|
||||||
|
"modified": "2021-12-10 19:27:14.654220",
|
||||||
|
"modified_by": "Administrator",
|
||||||
|
"module": "Accounts",
|
||||||
|
"name": "Deferred Revenue and Expense",
|
||||||
|
"owner": "Administrator",
|
||||||
|
"prepared_report": 0,
|
||||||
|
"ref_doctype": "GL Entry",
|
||||||
|
"report_name": "Deferred Revenue and Expense",
|
||||||
|
"report_type": "Script Report",
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"role": "Accounts User"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Accounts Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"role": "Auditor"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -0,0 +1,440 @@
|
|||||||
|
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||||
|
# License: MIT. See LICENSE
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import _, qb
|
||||||
|
from frappe.query_builder import Column, functions
|
||||||
|
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded
|
||||||
|
|
||||||
|
from erpnext.accounts.report.financial_statements import get_period_list
|
||||||
|
|
||||||
|
|
||||||
|
class Deferred_Item(object):
|
||||||
|
"""
|
||||||
|
Helper class for processing items with deferred revenue/expense
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, item, inv, gle_entries):
|
||||||
|
self.name = item
|
||||||
|
self.parent = inv.name
|
||||||
|
self.item_name = gle_entries[0].item_name
|
||||||
|
self.service_start_date = gle_entries[0].service_start_date
|
||||||
|
self.service_end_date = gle_entries[0].service_end_date
|
||||||
|
self.base_net_amount = gle_entries[0].base_net_amount
|
||||||
|
self.filters = inv.filters
|
||||||
|
self.period_list = inv.period_list
|
||||||
|
|
||||||
|
if gle_entries[0].deferred_revenue_account:
|
||||||
|
self.type = "Deferred Sale Item"
|
||||||
|
self.deferred_account = gle_entries[0].deferred_revenue_account
|
||||||
|
elif gle_entries[0].deferred_expense_account:
|
||||||
|
self.type = "Deferred Purchase Item"
|
||||||
|
self.deferred_account = gle_entries[0].deferred_expense_account
|
||||||
|
|
||||||
|
self.gle_entries = []
|
||||||
|
# holds period wise total for item
|
||||||
|
self.period_total = []
|
||||||
|
self.last_entry_date = self.service_start_date
|
||||||
|
|
||||||
|
if gle_entries:
|
||||||
|
self.gle_entries = gle_entries
|
||||||
|
for x in self.gle_entries:
|
||||||
|
if self.get_amount(x):
|
||||||
|
self.last_entry_date = x.gle_posting_date
|
||||||
|
|
||||||
|
def report_data(self):
|
||||||
|
"""
|
||||||
|
Generate report data for output
|
||||||
|
"""
|
||||||
|
ret_data = frappe._dict({"name": self.item_name})
|
||||||
|
for period in self.period_total:
|
||||||
|
ret_data[period.key] = period.total
|
||||||
|
ret_data.indent = 1
|
||||||
|
return ret_data
|
||||||
|
|
||||||
|
def get_amount(self, entry):
|
||||||
|
"""
|
||||||
|
For a given GL/Journal posting, get balance based on item type
|
||||||
|
"""
|
||||||
|
if self.type == "Deferred Sale Item":
|
||||||
|
return entry.debit - entry.credit
|
||||||
|
elif self.type == "Deferred Purchase Item":
|
||||||
|
return -(entry.credit - entry.debit)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
def get_item_total(self):
|
||||||
|
"""
|
||||||
|
Helper method - calculate booked amount. Includes simulated postings as well
|
||||||
|
"""
|
||||||
|
total = 0
|
||||||
|
for gle_posting in self.gle_entries:
|
||||||
|
total += self.get_amount(gle_posting)
|
||||||
|
|
||||||
|
return total
|
||||||
|
|
||||||
|
def calculate_amount(self, start_date, end_date):
|
||||||
|
"""
|
||||||
|
start_date, end_date - datetime.datetime.date
|
||||||
|
return - estimated amount to post for given period
|
||||||
|
Calculated based on already booked amount and item service period
|
||||||
|
"""
|
||||||
|
total_months = (
|
||||||
|
(self.service_end_date.year - self.service_start_date.year) * 12
|
||||||
|
+ (self.service_end_date.month - self.service_start_date.month)
|
||||||
|
+ 1
|
||||||
|
)
|
||||||
|
|
||||||
|
prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff(
|
||||||
|
get_last_day(self.service_end_date), get_first_day(self.service_start_date)
|
||||||
|
)
|
||||||
|
|
||||||
|
actual_months = rounded(total_months * prorate, 1)
|
||||||
|
|
||||||
|
already_booked_amount = self.get_item_total()
|
||||||
|
base_amount = self.base_net_amount / actual_months
|
||||||
|
|
||||||
|
if base_amount + already_booked_amount > self.base_net_amount:
|
||||||
|
base_amount = self.base_net_amount - already_booked_amount
|
||||||
|
|
||||||
|
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
|
||||||
|
partial_month = flt(date_diff(end_date, start_date)) / flt(
|
||||||
|
date_diff(get_last_day(end_date), get_first_day(start_date))
|
||||||
|
)
|
||||||
|
base_amount *= rounded(partial_month, 1)
|
||||||
|
|
||||||
|
return base_amount
|
||||||
|
|
||||||
|
def make_dummy_gle(self, name, date, amount):
|
||||||
|
"""
|
||||||
|
return - frappe._dict() of a dummy gle entry
|
||||||
|
"""
|
||||||
|
entry = frappe._dict(
|
||||||
|
{"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"}
|
||||||
|
)
|
||||||
|
if self.type == "Deferred Sale Item":
|
||||||
|
entry.debit = amount
|
||||||
|
elif self.type == "Deferred Purchase Item":
|
||||||
|
entry.credit = amount
|
||||||
|
return entry
|
||||||
|
|
||||||
|
def simulate_future_posting(self):
|
||||||
|
"""
|
||||||
|
simulate future posting by creating dummy gl entries. starts from the last posting date.
|
||||||
|
"""
|
||||||
|
if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date:
|
||||||
|
self.estimate_for_period_list = get_period_list(
|
||||||
|
self.filters.from_fiscal_year,
|
||||||
|
self.filters.to_fiscal_year,
|
||||||
|
add_days(self.last_entry_date, 1),
|
||||||
|
self.period_list[-1].to_date,
|
||||||
|
"Date Range",
|
||||||
|
"Monthly",
|
||||||
|
company=self.filters.company,
|
||||||
|
)
|
||||||
|
for period in self.estimate_for_period_list:
|
||||||
|
amount = self.calculate_amount(period.from_date, period.to_date)
|
||||||
|
gle = self.make_dummy_gle(period.key, period.to_date, amount)
|
||||||
|
self.gle_entries.append(gle)
|
||||||
|
|
||||||
|
def calculate_item_revenue_expense_for_period(self):
|
||||||
|
"""
|
||||||
|
calculate item postings for each period and update period_total list
|
||||||
|
"""
|
||||||
|
for period in self.period_list:
|
||||||
|
period_sum = 0
|
||||||
|
actual = 0
|
||||||
|
for posting in self.gle_entries:
|
||||||
|
# if period.from_date <= posting.posting_date <= period.to_date:
|
||||||
|
if period.from_date <= posting.gle_posting_date <= period.to_date:
|
||||||
|
period_sum += self.get_amount(posting)
|
||||||
|
if posting.posted == "posted":
|
||||||
|
actual += self.get_amount(posting)
|
||||||
|
|
||||||
|
self.period_total.append(
|
||||||
|
frappe._dict({"key": period.key, "total": period_sum, "actual": actual})
|
||||||
|
)
|
||||||
|
return self.period_total
|
||||||
|
|
||||||
|
|
||||||
|
class Deferred_Invoice(object):
|
||||||
|
def __init__(self, invoice, items, filters, period_list):
|
||||||
|
"""
|
||||||
|
Helper class for processing invoices with deferred revenue/expense items
|
||||||
|
invoice - string : invoice name
|
||||||
|
items - list : frappe._dict() with item details. Refer Deferred_Item for required fields
|
||||||
|
"""
|
||||||
|
self.name = invoice
|
||||||
|
self.posting_date = items[0].posting_date
|
||||||
|
self.filters = filters
|
||||||
|
self.period_list = period_list
|
||||||
|
# holds period wise total for invoice
|
||||||
|
self.period_total = []
|
||||||
|
|
||||||
|
if items[0].deferred_revenue_account:
|
||||||
|
self.type = "Sales"
|
||||||
|
elif items[0].deferred_expense_account:
|
||||||
|
self.type = "Purchase"
|
||||||
|
|
||||||
|
self.items = []
|
||||||
|
# for each uniq items
|
||||||
|
self.uniq_items = set([x.item for x in items])
|
||||||
|
for item in self.uniq_items:
|
||||||
|
self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item]))
|
||||||
|
|
||||||
|
def calculate_invoice_revenue_expense_for_period(self):
|
||||||
|
"""
|
||||||
|
calculate deferred revenue/expense for all items in invoice
|
||||||
|
"""
|
||||||
|
# initialize period_total list for invoice
|
||||||
|
for period in self.period_list:
|
||||||
|
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
|
||||||
|
|
||||||
|
for item in self.items:
|
||||||
|
item_total = item.calculate_item_revenue_expense_for_period()
|
||||||
|
# update invoice total
|
||||||
|
for idx, period in enumerate(self.period_list, 0):
|
||||||
|
self.period_total[idx].total += item_total[idx].total
|
||||||
|
self.period_total[idx].actual += item_total[idx].actual
|
||||||
|
return self.period_total
|
||||||
|
|
||||||
|
def estimate_future(self):
|
||||||
|
"""
|
||||||
|
create dummy GL entries for upcoming months for all items in invoice
|
||||||
|
"""
|
||||||
|
[item.simulate_future_posting() for item in self.items]
|
||||||
|
|
||||||
|
def report_data(self):
|
||||||
|
"""
|
||||||
|
generate report data for invoice, includes invoice total
|
||||||
|
"""
|
||||||
|
ret_data = []
|
||||||
|
inv_total = frappe._dict({"name": self.name})
|
||||||
|
for x in self.period_total:
|
||||||
|
inv_total[x.key] = x.total
|
||||||
|
inv_total.indent = 0
|
||||||
|
ret_data.append(inv_total)
|
||||||
|
list(map(lambda item: ret_data.append(item.report_data()), self.items))
|
||||||
|
return ret_data
|
||||||
|
|
||||||
|
|
||||||
|
class Deferred_Revenue_and_Expense_Report(object):
|
||||||
|
def __init__(self, filters=None):
|
||||||
|
"""
|
||||||
|
Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided
|
||||||
|
"""
|
||||||
|
|
||||||
|
# If no filters are provided, get user defaults
|
||||||
|
if not filters:
|
||||||
|
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": frappe.defaults.get_user_default("Company"),
|
||||||
|
"filter_based_on": "Fiscal Year",
|
||||||
|
"period_start_date": fiscal_year.year_start_date,
|
||||||
|
"period_end_date": fiscal_year.year_end_date,
|
||||||
|
"from_fiscal_year": fiscal_year.year,
|
||||||
|
"to_fiscal_year": fiscal_year.year,
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"type": "Revenue",
|
||||||
|
"with_upcoming_postings": True,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.filters = frappe._dict(filters)
|
||||||
|
|
||||||
|
self.period_list = None
|
||||||
|
self.deferred_invoices = []
|
||||||
|
# holds period wise total for report
|
||||||
|
self.period_total = []
|
||||||
|
|
||||||
|
def get_period_list(self):
|
||||||
|
"""
|
||||||
|
Figure out selected period based on filters
|
||||||
|
"""
|
||||||
|
self.period_list = get_period_list(
|
||||||
|
self.filters.from_fiscal_year,
|
||||||
|
self.filters.to_fiscal_year,
|
||||||
|
self.filters.period_start_date,
|
||||||
|
self.filters.period_end_date,
|
||||||
|
self.filters.filter_based_on,
|
||||||
|
self.filters.periodicity,
|
||||||
|
company=self.filters.company,
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_invoices(self):
|
||||||
|
"""
|
||||||
|
Get all sales and purchase invoices which has deferred revenue/expense items
|
||||||
|
"""
|
||||||
|
gle = qb.DocType("GL Entry")
|
||||||
|
# column doesn't have an alias option
|
||||||
|
posted = Column("posted")
|
||||||
|
|
||||||
|
if self.filters.type == "Revenue":
|
||||||
|
inv = qb.DocType("Sales Invoice")
|
||||||
|
inv_item = qb.DocType("Sales Invoice Item")
|
||||||
|
deferred_flag_field = inv_item["enable_deferred_revenue"]
|
||||||
|
deferred_account_field = inv_item["deferred_revenue_account"]
|
||||||
|
|
||||||
|
elif self.filters.type == "Expense":
|
||||||
|
inv = qb.DocType("Purchase Invoice")
|
||||||
|
inv_item = qb.DocType("Purchase Invoice Item")
|
||||||
|
deferred_flag_field = inv_item["enable_deferred_expense"]
|
||||||
|
deferred_account_field = inv_item["deferred_expense_account"]
|
||||||
|
|
||||||
|
query = (
|
||||||
|
qb.from_(inv_item)
|
||||||
|
.join(inv)
|
||||||
|
.on(inv.name == inv_item.parent)
|
||||||
|
.join(gle)
|
||||||
|
.on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account))
|
||||||
|
.select(
|
||||||
|
inv.name.as_("doc"),
|
||||||
|
inv.posting_date,
|
||||||
|
inv_item.name.as_("item"),
|
||||||
|
inv_item.item_name,
|
||||||
|
inv_item.service_start_date,
|
||||||
|
inv_item.service_end_date,
|
||||||
|
inv_item.base_net_amount,
|
||||||
|
deferred_account_field,
|
||||||
|
gle.posting_date.as_("gle_posting_date"),
|
||||||
|
functions.Sum(gle.debit).as_("debit"),
|
||||||
|
functions.Sum(gle.credit).as_("credit"),
|
||||||
|
posted,
|
||||||
|
)
|
||||||
|
.where(
|
||||||
|
(inv.docstatus == 1)
|
||||||
|
& (deferred_flag_field == 1)
|
||||||
|
& (
|
||||||
|
(
|
||||||
|
(self.period_list[0].from_date >= inv_item.service_start_date)
|
||||||
|
& (inv_item.service_end_date >= self.period_list[0].from_date)
|
||||||
|
)
|
||||||
|
| (
|
||||||
|
(inv_item.service_start_date >= self.period_list[0].from_date)
|
||||||
|
& (inv_item.service_start_date <= self.period_list[-1].to_date)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.groupby(inv.name, inv_item.name, gle.posting_date)
|
||||||
|
.orderby(gle.posting_date)
|
||||||
|
)
|
||||||
|
self.invoices = query.run(as_dict=True)
|
||||||
|
|
||||||
|
uniq_invoice = set([x.doc for x in self.invoices])
|
||||||
|
for inv in uniq_invoice:
|
||||||
|
self.deferred_invoices.append(
|
||||||
|
Deferred_Invoice(
|
||||||
|
inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def estimate_future(self):
|
||||||
|
"""
|
||||||
|
For all Invoices estimate upcoming postings
|
||||||
|
"""
|
||||||
|
for x in self.deferred_invoices:
|
||||||
|
x.estimate_future()
|
||||||
|
|
||||||
|
def calculate_revenue_and_expense(self):
|
||||||
|
"""
|
||||||
|
calculate the deferred revenue/expense for all invoices
|
||||||
|
"""
|
||||||
|
# initialize period_total list for report
|
||||||
|
for period in self.period_list:
|
||||||
|
self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0}))
|
||||||
|
|
||||||
|
for inv in self.deferred_invoices:
|
||||||
|
inv_total = inv.calculate_invoice_revenue_expense_for_period()
|
||||||
|
# calculate total for whole report
|
||||||
|
for idx, period in enumerate(self.period_list, 0):
|
||||||
|
self.period_total[idx].total += inv_total[idx].total
|
||||||
|
self.period_total[idx].actual += inv_total[idx].actual
|
||||||
|
|
||||||
|
def get_columns(self):
|
||||||
|
columns = []
|
||||||
|
columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1})
|
||||||
|
for period in self.period_list:
|
||||||
|
columns.append(
|
||||||
|
{
|
||||||
|
"label": _(period.label),
|
||||||
|
"fieldname": period.key,
|
||||||
|
"fieldtype": "Currency",
|
||||||
|
"read_only": 1,
|
||||||
|
})
|
||||||
|
return columns
|
||||||
|
|
||||||
|
def generate_report_data(self):
|
||||||
|
"""
|
||||||
|
Generate report data for all invoices. Adds total rows for revenue and expense
|
||||||
|
"""
|
||||||
|
ret = []
|
||||||
|
|
||||||
|
for inv in self.deferred_invoices:
|
||||||
|
ret += inv.report_data()
|
||||||
|
|
||||||
|
# empty row for padding
|
||||||
|
ret += [{}]
|
||||||
|
|
||||||
|
# add total row
|
||||||
|
if ret is not []:
|
||||||
|
if self.filters.type == "Revenue":
|
||||||
|
total_row = frappe._dict({"name": "Total Deferred Income"})
|
||||||
|
elif self.filters.type == "Expense":
|
||||||
|
total_row = frappe._dict({"name": "Total Deferred Expense"})
|
||||||
|
|
||||||
|
for idx, period in enumerate(self.period_list, 0):
|
||||||
|
total_row[period.key] = self.period_total[idx].total
|
||||||
|
ret.append(total_row)
|
||||||
|
|
||||||
|
return ret
|
||||||
|
|
||||||
|
def prepare_chart(self):
|
||||||
|
chart = {
|
||||||
|
"data": {
|
||||||
|
"labels": [period.label for period in self.period_list],
|
||||||
|
"datasets": [
|
||||||
|
{
|
||||||
|
"name": "Actual Posting",
|
||||||
|
"chartType": "bar",
|
||||||
|
"values": [x.actual for x in self.period_total],
|
||||||
|
}
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"type": "axis-mixed",
|
||||||
|
"height": 500,
|
||||||
|
"axisOptions": {"xAxisMode": "Tick", "xIsSeries": True},
|
||||||
|
"barOptions": {"stacked": False, "spaceRatio": 0.5},
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.filters.with_upcoming_postings:
|
||||||
|
chart["data"]["datasets"].append({
|
||||||
|
"name": "Expected",
|
||||||
|
"chartType": "line",
|
||||||
|
"values": [x.total for x in self.period_total]
|
||||||
|
})
|
||||||
|
|
||||||
|
return chart
|
||||||
|
|
||||||
|
def run(self, *args, **kwargs):
|
||||||
|
"""
|
||||||
|
Run report and generate data
|
||||||
|
"""
|
||||||
|
self.deferred_invoices.clear()
|
||||||
|
self.get_period_list()
|
||||||
|
self.get_invoices()
|
||||||
|
|
||||||
|
if self.filters.with_upcoming_postings:
|
||||||
|
self.estimate_future()
|
||||||
|
self.calculate_revenue_and_expense()
|
||||||
|
|
||||||
|
|
||||||
|
def execute(filters=None):
|
||||||
|
report = Deferred_Revenue_and_Expense_Report(filters=filters)
|
||||||
|
report.run()
|
||||||
|
|
||||||
|
columns = report.get_columns()
|
||||||
|
data = report.generate_report_data()
|
||||||
|
message = []
|
||||||
|
chart = report.prepare_chart()
|
||||||
|
|
||||||
|
return columns, data, message, chart
|
||||||
@@ -0,0 +1,253 @@
|
|||||||
|
import unittest
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
from frappe import qb
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
|
from erpnext.accounts.doctype.account.test_account import create_account
|
||||||
|
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||||
|
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||||
|
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
|
||||||
|
Deferred_Revenue_and_Expense_Report,
|
||||||
|
)
|
||||||
|
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||||
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
|
||||||
|
|
||||||
|
class TestDeferredRevenueAndExpense(unittest.TestCase):
|
||||||
|
@classmethod
|
||||||
|
def setUpClass(self):
|
||||||
|
clear_old_entries()
|
||||||
|
create_company()
|
||||||
|
|
||||||
|
def test_deferred_revenue(self):
|
||||||
|
# created deferred expense accounts, if not found
|
||||||
|
deferred_revenue_account = create_account(
|
||||||
|
account_name="Deferred Revenue",
|
||||||
|
parent_account="Current Liabilities - _CD",
|
||||||
|
company="_Test Company DR",
|
||||||
|
)
|
||||||
|
|
||||||
|
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||||
|
acc_settings.book_deferred_entries_based_on = "Months"
|
||||||
|
acc_settings.save()
|
||||||
|
|
||||||
|
customer = frappe.new_doc("Customer")
|
||||||
|
customer.customer_name = "_Test Customer DR"
|
||||||
|
customer.type = "Individual"
|
||||||
|
customer.insert()
|
||||||
|
|
||||||
|
item = create_item(
|
||||||
|
"_Test Internet Subscription",
|
||||||
|
is_stock_item=0,
|
||||||
|
warehouse="All Warehouses - _CD",
|
||||||
|
company="_Test Company DR",
|
||||||
|
)
|
||||||
|
item.enable_deferred_revenue = 1
|
||||||
|
item.deferred_revenue_account = deferred_revenue_account
|
||||||
|
item.no_of_months = 3
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
si = create_sales_invoice(
|
||||||
|
item=item.name,
|
||||||
|
company="_Test Company DR",
|
||||||
|
customer="_Test Customer DR",
|
||||||
|
debit_to="Debtors - _CD",
|
||||||
|
posting_date="2021-05-01",
|
||||||
|
parent_cost_center="Main - _CD",
|
||||||
|
cost_center="Main - _CD",
|
||||||
|
do_not_submit=True,
|
||||||
|
rate=300,
|
||||||
|
price_list_rate=300,
|
||||||
|
)
|
||||||
|
si.items[0].enable_deferred_revenue = 1
|
||||||
|
si.items[0].service_start_date = "2021-05-01"
|
||||||
|
si.items[0].service_end_date = "2021-08-01"
|
||||||
|
si.items[0].deferred_revenue_account = deferred_revenue_account
|
||||||
|
si.items[0].income_account = "Sales - _CD"
|
||||||
|
si.save()
|
||||||
|
si.submit()
|
||||||
|
|
||||||
|
pda = frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
doctype="Process Deferred Accounting",
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2021-05-01",
|
||||||
|
end_date="2021-08-01",
|
||||||
|
type="Income",
|
||||||
|
company="_Test Company DR",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pda.insert()
|
||||||
|
pda.submit()
|
||||||
|
|
||||||
|
# execute report
|
||||||
|
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": frappe.defaults.get_user_default("Company"),
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"period_start_date": "2021-05-01",
|
||||||
|
"period_end_date": "2021-08-01",
|
||||||
|
"from_fiscal_year": fiscal_year.year,
|
||||||
|
"to_fiscal_year": fiscal_year.year,
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"type": "Revenue",
|
||||||
|
"with_upcoming_postings": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||||
|
report.run()
|
||||||
|
expected = [
|
||||||
|
{"key": "may_2021", "total": 100.0, "actual": 100.0},
|
||||||
|
{"key": "jun_2021", "total": 100.0, "actual": 100.0},
|
||||||
|
{"key": "jul_2021", "total": 100.0, "actual": 100.0},
|
||||||
|
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||||
|
]
|
||||||
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
def test_deferred_expense(self):
|
||||||
|
# created deferred expense accounts, if not found
|
||||||
|
deferred_expense_account = create_account(
|
||||||
|
account_name="Deferred Expense",
|
||||||
|
parent_account="Current Assets - _CD",
|
||||||
|
company="_Test Company DR",
|
||||||
|
)
|
||||||
|
|
||||||
|
acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
|
||||||
|
acc_settings.book_deferred_entries_based_on = "Months"
|
||||||
|
acc_settings.save()
|
||||||
|
|
||||||
|
supplier = create_supplier(
|
||||||
|
supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company"
|
||||||
|
)
|
||||||
|
supplier.save()
|
||||||
|
|
||||||
|
item = create_item(
|
||||||
|
"_Test Office Desk",
|
||||||
|
is_stock_item=0,
|
||||||
|
warehouse="All Warehouses - _CD",
|
||||||
|
company="_Test Company DR",
|
||||||
|
)
|
||||||
|
item.enable_deferred_expense = 1
|
||||||
|
item.deferred_expense_account = deferred_expense_account
|
||||||
|
item.no_of_months_exp = 3
|
||||||
|
item.save()
|
||||||
|
|
||||||
|
pi = make_purchase_invoice(
|
||||||
|
item=item.name,
|
||||||
|
company="_Test Company DR",
|
||||||
|
supplier="_Test Furniture Supplier",
|
||||||
|
is_return=False,
|
||||||
|
update_stock=False,
|
||||||
|
posting_date=frappe.utils.datetime.date(2021, 5, 1),
|
||||||
|
parent_cost_center="Main - _CD",
|
||||||
|
cost_center="Main - _CD",
|
||||||
|
do_not_save=True,
|
||||||
|
rate=300,
|
||||||
|
price_list_rate=300,
|
||||||
|
warehouse="All Warehouses - _CD",
|
||||||
|
qty=1,
|
||||||
|
)
|
||||||
|
pi.set_posting_time = True
|
||||||
|
pi.items[0].enable_deferred_expense = 1
|
||||||
|
pi.items[0].service_start_date = "2021-05-01"
|
||||||
|
pi.items[0].service_end_date = "2021-08-01"
|
||||||
|
pi.items[0].deferred_expense_account = deferred_expense_account
|
||||||
|
pi.items[0].expense_account = "Office Maintenance Expenses - _CD"
|
||||||
|
pi.save()
|
||||||
|
pi.submit()
|
||||||
|
|
||||||
|
pda = frappe.get_doc(
|
||||||
|
dict(
|
||||||
|
doctype="Process Deferred Accounting",
|
||||||
|
posting_date=nowdate(),
|
||||||
|
start_date="2021-05-01",
|
||||||
|
end_date="2021-08-01",
|
||||||
|
type="Expense",
|
||||||
|
company="_Test Company DR",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
pda.insert()
|
||||||
|
pda.submit()
|
||||||
|
|
||||||
|
# execute report
|
||||||
|
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year"))
|
||||||
|
self.filters = frappe._dict(
|
||||||
|
{
|
||||||
|
"company": frappe.defaults.get_user_default("Company"),
|
||||||
|
"filter_based_on": "Date Range",
|
||||||
|
"period_start_date": "2021-05-01",
|
||||||
|
"period_end_date": "2021-08-01",
|
||||||
|
"from_fiscal_year": fiscal_year.year,
|
||||||
|
"to_fiscal_year": fiscal_year.year,
|
||||||
|
"periodicity": "Monthly",
|
||||||
|
"type": "Expense",
|
||||||
|
"with_upcoming_postings": False,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
report = Deferred_Revenue_and_Expense_Report(filters=self.filters)
|
||||||
|
report.run()
|
||||||
|
expected = [
|
||||||
|
{"key": "may_2021", "total": -100.0, "actual": -100.0},
|
||||||
|
{"key": "jun_2021", "total": -100.0, "actual": -100.0},
|
||||||
|
{"key": "jul_2021", "total": -100.0, "actual": -100.0},
|
||||||
|
{"key": "aug_2021", "total": 0, "actual": 0},
|
||||||
|
]
|
||||||
|
self.assertEqual(report.period_total, expected)
|
||||||
|
|
||||||
|
|
||||||
|
def create_company():
|
||||||
|
company = frappe.db.exists("Company", "_Test Company DR")
|
||||||
|
if not company:
|
||||||
|
company = frappe.new_doc("Company")
|
||||||
|
company.company_name = "_Test Company DR"
|
||||||
|
company.default_currency = "INR"
|
||||||
|
company.chart_of_accounts = "Standard"
|
||||||
|
company.insert()
|
||||||
|
|
||||||
|
|
||||||
|
def clear_old_entries():
|
||||||
|
item = qb.DocType("Item")
|
||||||
|
account = qb.DocType("Account")
|
||||||
|
customer = qb.DocType("Customer")
|
||||||
|
supplier = qb.DocType("Supplier")
|
||||||
|
sinv = qb.DocType("Sales Invoice")
|
||||||
|
sinv_item = qb.DocType("Sales Invoice Item")
|
||||||
|
pinv = qb.DocType("Purchase Invoice")
|
||||||
|
pinv_item = qb.DocType("Purchase Invoice Item")
|
||||||
|
|
||||||
|
qb.from_(account).delete().where(
|
||||||
|
(account.account_name == "Deferred Revenue")
|
||||||
|
| (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR")
|
||||||
|
).run()
|
||||||
|
qb.from_(item).delete().where(
|
||||||
|
(item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent")
|
||||||
|
).run()
|
||||||
|
qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run()
|
||||||
|
qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run()
|
||||||
|
|
||||||
|
# delete existing invoices with deferred items
|
||||||
|
deferred_invoices = (
|
||||||
|
qb.from_(sinv)
|
||||||
|
.join(sinv_item)
|
||||||
|
.on(sinv.name == sinv_item.parent)
|
||||||
|
.select(sinv.name)
|
||||||
|
.where(sinv_item.enable_deferred_revenue == 1)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
if deferred_invoices:
|
||||||
|
qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run()
|
||||||
|
|
||||||
|
deferred_invoices = (
|
||||||
|
qb.from_(pinv)
|
||||||
|
.join(pinv_item)
|
||||||
|
.on(pinv.name == pinv_item.parent)
|
||||||
|
.select(pinv.name)
|
||||||
|
.where(pinv_item.enable_deferred_expense == 1)
|
||||||
|
.run()
|
||||||
|
)
|
||||||
|
if deferred_invoices:
|
||||||
|
qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run()
|
||||||
@@ -36,12 +36,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
|||||||
posting_date = entry.posting_date
|
posting_date = entry.posting_date
|
||||||
voucher_type = entry.voucher_type
|
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:
|
if entry.account in tds_accounts:
|
||||||
tds_deducted += (entry.credit - entry.debit)
|
tds_deducted += (entry.credit - entry.debit)
|
||||||
|
|
||||||
total_amount_credited += (entry.credit - entry.debit)
|
total_amount_credited += (entry.credit - entry.debit)
|
||||||
|
|
||||||
if rate and tds_deducted:
|
if tds_deducted:
|
||||||
row = {
|
row = {
|
||||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
||||||
'supplier': supplier_map.get(supplier, {}).get('name')
|
'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():
|
def get_supplier_pan_map():
|
||||||
supplier_map = frappe._dict()
|
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:
|
for d in suppliers:
|
||||||
supplier_map[d.name] = d
|
supplier_map[d.name] = d
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ frappe.ui.form.on('Asset', {
|
|||||||
|
|
||||||
if (frm.doc.status != 'Fully Depreciated') {
|
if (frm.doc.status != 'Fully Depreciated') {
|
||||||
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
frm.add_custom_button(__("Adjust Asset Value"), function() {
|
||||||
frm.trigger("create_asset_adjustment");
|
frm.trigger("create_asset_value_adjustment");
|
||||||
}, __("Manage"));
|
}, __("Manage"));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,14 +322,14 @@ frappe.ui.form.on('Asset', {
|
|||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
create_asset_adjustment: function(frm) {
|
create_asset_value_adjustment: function(frm) {
|
||||||
frappe.call({
|
frappe.call({
|
||||||
args: {
|
args: {
|
||||||
"asset": frm.doc.name,
|
"asset": frm.doc.name,
|
||||||
"asset_category": frm.doc.asset_category,
|
"asset_category": frm.doc.asset_category,
|
||||||
"company": frm.doc.company
|
"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,
|
freeze: 1,
|
||||||
callback: function(r) {
|
callback: function(r) {
|
||||||
var doclist = frappe.model.sync(r.message);
|
var doclist = frappe.model.sync(r.message);
|
||||||
|
|||||||
@@ -185,83 +185,84 @@ class Asset(AccountsController):
|
|||||||
if not self.available_for_use_date:
|
if not self.available_for_use_date:
|
||||||
return
|
return
|
||||||
|
|
||||||
for d in self.get('finance_books'):
|
start = self.clear_depreciation_schedule()
|
||||||
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
|
# value_after_depreciation - current Asset value
|
||||||
if self.docstatus == 1 and d.value_after_depreciation:
|
if self.docstatus == 1 and finance_book.value_after_depreciation:
|
||||||
value_after_depreciation = flt(d.value_after_depreciation)
|
value_after_depreciation = flt(finance_book.value_after_depreciation)
|
||||||
else:
|
else:
|
||||||
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
value_after_depreciation = (flt(self.gross_purchase_amount) -
|
||||||
flt(self.opening_accumulated_depreciation))
|
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)
|
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:
|
if has_pro_rata:
|
||||||
number_of_pending_depreciations += 1
|
number_of_pending_depreciations += 1
|
||||||
|
|
||||||
skip_row = False
|
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 depreciation is already completed (for double declining balance)
|
||||||
if skip_row: continue
|
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:
|
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
|
||||||
schedule_date = add_months(d.depreciation_start_date,
|
schedule_date = add_months(finance_book.depreciation_start_date,
|
||||||
n * cint(d.frequency_of_depreciation))
|
n * cint(finance_book.frequency_of_depreciation))
|
||||||
|
|
||||||
# schedule date will be a year later from start date
|
# schedule date will be a year later from start date
|
||||||
# so monthly schedule date is calculated by removing 11 months from it
|
# 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 asset is being sold
|
||||||
if date_of_sale:
|
if date_of_sale:
|
||||||
from_date = self.get_from_date(d.finance_book)
|
from_date = self.get_from_date(finance_book.finance_book)
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||||
from_date, date_of_sale)
|
from_date, date_of_sale)
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
self.append("schedules", {
|
self.append("schedules", {
|
||||||
"schedule_date": date_of_sale,
|
"schedule_date": date_of_sale,
|
||||||
"depreciation_amount": depreciation_amount,
|
"depreciation_amount": depreciation_amount,
|
||||||
"depreciation_method": d.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": d.finance_book,
|
"finance_book": finance_book.finance_book,
|
||||||
"finance_book_id": d.idx
|
"finance_book_id": finance_book.idx
|
||||||
})
|
})
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
# For first row
|
# For first row
|
||||||
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
if has_pro_rata and not self.opening_accumulated_depreciation and n==0:
|
||||||
depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
|
depreciation_amount, days, months = self.get_pro_rata_amt(finance_book, depreciation_amount,
|
||||||
self.available_for_use_date, d.depreciation_start_date)
|
self.available_for_use_date, finance_book.depreciation_start_date)
|
||||||
|
|
||||||
# For first depr schedule date will be the 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
|
# 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
|
# For last row
|
||||||
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
elif has_pro_rata and n == cint(number_of_pending_depreciations) - 1:
|
||||||
if not self.flags.increase_in_asset_life:
|
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
|
# 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,
|
self.to_date = add_months(self.available_for_use_date,
|
||||||
(n + self.number_of_depreciations_booked) * 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_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, schedule_date, self.to_date)
|
||||||
|
|
||||||
depreciation_amount = self.get_adjusted_depreciation_amount(depreciation_amount_without_pro_rata,
|
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)
|
monthly_schedule_date = add_months(schedule_date, 1)
|
||||||
schedule_date = add_days(schedule_date, days)
|
schedule_date = add_days(schedule_date, days)
|
||||||
@@ -272,10 +273,10 @@ class Asset(AccountsController):
|
|||||||
self.precision("gross_purchase_amount"))
|
self.precision("gross_purchase_amount"))
|
||||||
|
|
||||||
# Adjust depreciation amount in the last period based on the expected value after useful life
|
# 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
|
if finance_book.expected_value_after_useful_life and ((n == cint(number_of_pending_depreciations) - 1
|
||||||
and value_after_depreciation != d.expected_value_after_useful_life)
|
and value_after_depreciation != finance_book.expected_value_after_useful_life)
|
||||||
or value_after_depreciation < d.expected_value_after_useful_life):
|
or value_after_depreciation < finance_book.expected_value_after_useful_life):
|
||||||
depreciation_amount += (value_after_depreciation - d.expected_value_after_useful_life)
|
depreciation_amount += (value_after_depreciation - finance_book.expected_value_after_useful_life)
|
||||||
skip_row = True
|
skip_row = True
|
||||||
|
|
||||||
if depreciation_amount > 0:
|
if depreciation_amount > 0:
|
||||||
@@ -285,7 +286,7 @@ class Asset(AccountsController):
|
|||||||
# In pro rata case, for first and last depreciation, month range would be different
|
# In pro rata case, for first and last depreciation, month range would be different
|
||||||
month_range = months \
|
month_range = months \
|
||||||
if (has_pro_rata and n==0) or (has_pro_rata and n == cint(number_of_pending_depreciations) - 1) \
|
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):
|
for r in range(month_range):
|
||||||
if (has_pro_rata and n == 0):
|
if (has_pro_rata and n == 0):
|
||||||
@@ -311,27 +312,52 @@ class Asset(AccountsController):
|
|||||||
self.append("schedules", {
|
self.append("schedules", {
|
||||||
"schedule_date": date,
|
"schedule_date": date,
|
||||||
"depreciation_amount": amount,
|
"depreciation_amount": amount,
|
||||||
"depreciation_method": d.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": d.finance_book,
|
"finance_book": finance_book.finance_book,
|
||||||
"finance_book_id": d.idx
|
"finance_book_id": finance_book.idx
|
||||||
})
|
})
|
||||||
else:
|
else:
|
||||||
self.append("schedules", {
|
self.append("schedules", {
|
||||||
"schedule_date": schedule_date,
|
"schedule_date": schedule_date,
|
||||||
"depreciation_amount": depreciation_amount,
|
"depreciation_amount": depreciation_amount,
|
||||||
"depreciation_method": d.depreciation_method,
|
"depreciation_method": finance_book.depreciation_method,
|
||||||
"finance_book": d.finance_book,
|
"finance_book": finance_book.finance_book,
|
||||||
"finance_book_id": d.idx
|
"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):
|
def clear_depreciation_schedule(self):
|
||||||
start = 0
|
start = []
|
||||||
for n in range(len(self.schedules)):
|
num_of_depreciations_completed = 0
|
||||||
if not self.schedules[n].journal_entry:
|
depr_schedule = []
|
||||||
del self.schedules[n:]
|
|
||||||
start = n
|
for schedule in self.get('schedules'):
|
||||||
break
|
|
||||||
|
# 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
|
return start
|
||||||
|
|
||||||
def get_from_date(self, finance_book):
|
def get_from_date(self, finance_book):
|
||||||
@@ -469,7 +495,6 @@ class Asset(AccountsController):
|
|||||||
|
|
||||||
asset_value_after_full_schedule = flt(
|
asset_value_after_full_schedule = flt(
|
||||||
flt(self.gross_purchase_amount) -
|
flt(self.gross_purchase_amount) -
|
||||||
flt(self.opening_accumulated_depreciation) -
|
|
||||||
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
flt(accumulated_depreciation_after_full_schedule), self.precision('gross_purchase_amount'))
|
||||||
|
|
||||||
if (row.expected_value_after_useful_life and
|
if (row.expected_value_after_useful_life and
|
||||||
@@ -731,14 +756,14 @@ def create_asset_repair(asset, asset_name):
|
|||||||
return asset_repair
|
return asset_repair
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def create_asset_adjustment(asset, asset_category, company):
|
def create_asset_value_adjustment(asset, asset_category, company):
|
||||||
asset_maintenance = frappe.get_doc("Asset Value Adjustment")
|
asset_value_adjustment = frappe.new_doc("Asset Value Adjustment")
|
||||||
asset_maintenance.update({
|
asset_value_adjustment.update({
|
||||||
"asset": asset,
|
"asset": asset,
|
||||||
"company": company,
|
"company": company,
|
||||||
"asset_category": asset_category
|
"asset_category": asset_category
|
||||||
})
|
})
|
||||||
return asset_maintenance
|
return asset_value_adjustment
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def transfer_asset(args):
|
def transfer_asset(args):
|
||||||
|
|||||||
@@ -955,6 +955,82 @@ class TestDepreciationBasics(AssetSetup):
|
|||||||
|
|
||||||
self.assertEqual(len(asset.schedules), 1)
|
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):
|
def test_depreciation_entry_cancellation(self):
|
||||||
asset = create_asset(
|
asset = create_asset(
|
||||||
item_code = "Macbook Pro",
|
item_code = "Macbook Pro",
|
||||||
|
|||||||
@@ -72,6 +72,7 @@ class PurchaseOrder(BuyingController):
|
|||||||
self.create_raw_materials_supplied("supplied_items")
|
self.create_raw_materials_supplied("supplied_items")
|
||||||
self.set_received_qty_for_drop_ship_items()
|
self.set_received_qty_for_drop_ship_items()
|
||||||
validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_order_reference)
|
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):
|
def validate_with_previous_doc(self):
|
||||||
super(PurchaseOrder, self).validate_with_previous_doc({
|
super(PurchaseOrder, self).validate_with_previous_doc({
|
||||||
|
|||||||
@@ -124,6 +124,14 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
dialog.show()
|
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) => {
|
preview: (frm) => {
|
||||||
let dialog = new frappe.ui.Dialog({
|
let dialog = new frappe.ui.Dialog({
|
||||||
title: __('Preview Email'),
|
title: __('Preview Email'),
|
||||||
@@ -184,7 +192,13 @@ frappe.ui.form.on("Request for Quotation",{
|
|||||||
dialog.show();
|
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",{
|
frappe.ui.form.on("Request for Quotation Supplier",{
|
||||||
supplier: function(frm, cdt, cdn) {
|
supplier: function(frm, cdt, cdn) {
|
||||||
var d = locals[cdt][cdn]
|
var d = locals[cdt][cdn]
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
"vendor",
|
"vendor",
|
||||||
"column_break1",
|
"column_break1",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
|
"schedule_date",
|
||||||
"status",
|
"status",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"suppliers_section",
|
"suppliers_section",
|
||||||
@@ -246,16 +247,22 @@
|
|||||||
"fieldname": "sec_break_email_2",
|
"fieldname": "sec_break_email_2",
|
||||||
"fieldtype": "Section Break",
|
"fieldtype": "Section Break",
|
||||||
"hide_border": 1
|
"hide_border": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "schedule_date",
|
||||||
|
"fieldtype": "Date",
|
||||||
|
"label": "Required Date"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2020-11-05 22:04:29.017134",
|
"modified": "2021-11-24 17:47:49.909000",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Request for Quotation",
|
"name": "Request for Quotation",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
"company",
|
"company",
|
||||||
"transaction_date",
|
"transaction_date",
|
||||||
"valid_till",
|
"valid_till",
|
||||||
|
"quotation_number",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"address_section",
|
"address_section",
|
||||||
"supplier_address",
|
"supplier_address",
|
||||||
@@ -797,6 +798,11 @@
|
|||||||
"fieldtype": "Date",
|
"fieldtype": "Date",
|
||||||
"in_list_view": 1,
|
"in_list_view": 1,
|
||||||
"label": "Valid Till"
|
"label": "Valid Till"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "quotation_number",
|
||||||
|
"fieldtype": "Data",
|
||||||
|
"label": "Quotation Number"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"icon": "fa fa-shopping-cart",
|
"icon": "fa fa-shopping-cart",
|
||||||
@@ -804,10 +810,11 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"is_submittable": 1,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-04-19 00:58:20.995491",
|
"modified": "2021-12-11 06:43:20.924080",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Supplier Quotation",
|
"name": "Supplier Quotation",
|
||||||
|
"naming_rule": "By \"Naming Series\" field",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [
|
"permissions": [
|
||||||
{
|
{
|
||||||
|
|||||||
22
erpnext/controllers/tests/test_transaction_base.py
Normal file
22
erpnext/controllers/tests/test_transaction_base.py
Normal 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)
|
||||||
|
|
||||||
@@ -91,8 +91,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"migration_hash": "3ae78b12dd1c64d551736c6e82092f90",
|
"modified": "2021-11-03 10:00:36.883496",
|
||||||
"modified": "2021-11-03 09:00:36.883496",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "CRM Settings",
|
"name": "CRM Settings",
|
||||||
|
|||||||
@@ -510,8 +510,7 @@
|
|||||||
"icon": "fa fa-info-sign",
|
"icon": "fa fa-info-sign",
|
||||||
"idx": 195,
|
"idx": 195,
|
||||||
"links": [],
|
"links": [],
|
||||||
"migration_hash": "d87c646ea2579b6900197fd41e6c5c5a",
|
"modified": "2021-10-21 12:04:30.151379",
|
||||||
"modified": "2021-10-21 11:04:30.151379",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "CRM",
|
"module": "CRM",
|
||||||
"name": "Opportunity",
|
"name": "Opportunity",
|
||||||
|
|||||||
@@ -115,8 +115,7 @@
|
|||||||
],
|
],
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"migration_hash": "8ca1ea3309ed28547b19da8e6e27e96f",
|
"modified": "2021-11-30 12:17:24.647979",
|
||||||
"modified": "2021-11-30 11:17:24.647979",
|
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "ERPNext Integrations",
|
"module": "ERPNext Integrations",
|
||||||
"name": "TaxJar Settings",
|
"name": "TaxJar Settings",
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ doc_events = {
|
|||||||
},
|
},
|
||||||
"Communication": {
|
"Communication": {
|
||||||
"on_update": [
|
"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"
|
"erpnext.support.doctype.issue.issue.set_first_response_time"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
@@ -265,6 +265,9 @@ doc_events = {
|
|||||||
"erpnext.regional.india.utils.update_taxable_values"
|
"erpnext.regional.india.utils.update_taxable_values"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
"POS Invoice": {
|
||||||
|
"on_submit": ["erpnext.regional.saudi_arabia.utils.create_qr_code"]
|
||||||
|
},
|
||||||
"Purchase Invoice": {
|
"Purchase Invoice": {
|
||||||
"validate": [
|
"validate": [
|
||||||
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
|
"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.erpnext_integrations.doctype.plaid_settings.plaid_settings.automatic_synchronization",
|
||||||
"erpnext.projects.doctype.project.project.hourly_reminder",
|
"erpnext.projects.doctype.project.project.hourly_reminder",
|
||||||
"erpnext.projects.doctype.project.project.collect_project_status",
|
"erpnext.projects.doctype.project.project.collect_project_status",
|
||||||
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
|
"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"
|
|
||||||
],
|
],
|
||||||
"hourly_long": [
|
"hourly_long": [
|
||||||
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
|
||||||
|
|||||||
@@ -21,7 +21,11 @@ def get_data():
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
'label': _('Lifecycle'),
|
'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'),
|
'label': _('Shift'),
|
||||||
|
|||||||
0
erpnext/hr/doctype/exit_interview/__init__.py
Normal file
0
erpnext/hr/doctype/exit_interview/__init__.py
Normal file
38
erpnext/hr/doctype/exit_interview/exit_interview.js
Normal file
38
erpnext/hr/doctype/exit_interview/exit_interview.js
Normal 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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
246
erpnext/hr/doctype/exit_interview/exit_interview.json
Normal file
246
erpnext/hr/doctype/exit_interview/exit_interview.json
Normal 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
|
||||||
|
}
|
||||||
131
erpnext/hr/doctype/exit_interview/exit_interview.py
Normal file
131
erpnext/hr/doctype/exit_interview/exit_interview.py
Normal 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)
|
||||||
27
erpnext/hr/doctype/exit_interview/exit_interview_list.js
Normal file
27
erpnext/hr/doctype/exit_interview/exit_interview_list.js
Normal 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
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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>
|
||||||
118
erpnext/hr/doctype/exit_interview/test_exit_interview.py
Normal file
118
erpnext/hr/doctype/exit_interview/test_exit_interview.py
Normal 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('employeeexitint1@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('employeeexitint2@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
|
||||||
@@ -36,7 +36,11 @@
|
|||||||
"remind_before",
|
"remind_before",
|
||||||
"column_break_4",
|
"column_break_4",
|
||||||
"send_interview_feedback_reminder",
|
"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": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@@ -226,13 +230,34 @@
|
|||||||
"fieldname": "check_vacancies",
|
"fieldname": "check_vacancies",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Check Vacancies On Job Offer Creation"
|
"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",
|
"icon": "fa fa-cog",
|
||||||
"idx": 1,
|
"idx": 1,
|
||||||
"issingle": 1,
|
"issingle": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-10-01 23:46:11.098236",
|
"modified": "2021-12-05 14:48:10.884253",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR Settings",
|
"name": "HR Settings",
|
||||||
|
|||||||
@@ -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 }}"
|
||||||
|
}
|
||||||
@@ -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>
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
# import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def get_context(context):
|
||||||
|
# do your magic here
|
||||||
|
pass
|
||||||
0
erpnext/hr/report/employee_exits/__init__.py
Normal file
0
erpnext/hr/report/employee_exits/__init__.py
Normal file
77
erpnext/hr/report/employee_exits/employee_exits.js
Normal file
77
erpnext/hr/report/employee_exits/employee_exits.js
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
33
erpnext/hr/report/employee_exits/employee_exits.json
Normal file
33
erpnext/hr/report/employee_exits/employee_exits.json
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
230
erpnext/hr/report/employee_exits/employee_exits.py
Normal file
230
erpnext/hr/report/employee_exits/employee_exits.py
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
# 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()
|
||||||
|
.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)))
|
||||||
|
).orderby(employee.relieving_date, order=Order.asc)
|
||||||
|
)
|
||||||
|
|
||||||
|
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'
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
242
erpnext/hr/report/employee_exits/test_employee_exits.py
Normal file
242
erpnext/hr/report/employee_exits/test_employee_exits.py
Normal 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()
|
||||||
@@ -5,7 +5,7 @@
|
|||||||
"label": "Outgoing Salary"
|
"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 & 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",
|
"creation": "2020-03-02 15:48:58.322521",
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workspace",
|
"doctype": "Workspace",
|
||||||
@@ -15,14 +15,6 @@
|
|||||||
"idx": 0,
|
"idx": 0,
|
||||||
"label": "HR",
|
"label": "HR",
|
||||||
"links": [
|
"links": [
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Employee",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -111,14 +103,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Employee Lifecycle",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Job Applicant",
|
"dependencies": "Job Applicant",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -227,14 +211,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Shift Management",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -268,14 +244,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Leaves",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -386,14 +354,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Attendance",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Employee",
|
"dependencies": "Employee",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -449,14 +409,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Expense Claims",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Employee",
|
"dependencies": "Employee",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -489,14 +441,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Settings",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -530,14 +474,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Fleet Management",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
"is_query_report": 0,
|
"is_query_report": 0,
|
||||||
@@ -581,14 +517,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Recruitment",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "",
|
"dependencies": "",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -808,14 +736,6 @@
|
|||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"type": "Link"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"hidden": 0,
|
|
||||||
"is_query_report": 0,
|
|
||||||
"label": "Key Reports",
|
|
||||||
"link_count": 0,
|
|
||||||
"onboard": 0,
|
|
||||||
"type": "Card Break"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"dependencies": "Attendance",
|
"dependencies": "Attendance",
|
||||||
"hidden": 0,
|
"hidden": 0,
|
||||||
@@ -933,9 +853,796 @@
|
|||||||
"link_type": "Report",
|
"link_type": "Report",
|
||||||
"onboard": 0,
|
"onboard": 0,
|
||||||
"type": "Link"
|
"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",
|
"modified_by": "Administrator",
|
||||||
"module": "HR",
|
"module": "HR",
|
||||||
"name": "HR",
|
"name": "HR",
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, today
|
from frappe.utils import add_months, today
|
||||||
|
|
||||||
from erpnext import get_company_currency
|
from erpnext import get_company_currency
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
from .blanket_order import make_order
|
from .blanket_order import make_order
|
||||||
|
|
||||||
|
|
||||||
class TestBlanketOrder(unittest.TestCase):
|
class TestBlanketOrder(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
frappe.flags.args = frappe._dict()
|
frappe.flags.args = frappe._dict()
|
||||||
|
|
||||||
|
|||||||
@@ -922,7 +922,7 @@ def validate_bom_no(item, bom_no):
|
|||||||
rm_item_exists = True
|
rm_item_exists = True
|
||||||
if bom.item.lower() == item.lower() or \
|
if bom.item.lower() == item.lower() or \
|
||||||
bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower():
|
bom.item.lower() == cstr(frappe.db.get_value("Item", item, "variant_of")).lower():
|
||||||
rm_item_exists = True
|
rm_item_exists = True
|
||||||
if not rm_item_exists:
|
if not rm_item_exists:
|
||||||
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
|
frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item))
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
from collections import deque
|
from collections import deque
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
@@ -18,10 +17,11 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
|
|||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
)
|
)
|
||||||
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
from erpnext.tests.test_subcontracting import set_backflush_based_on
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_records = frappe.get_test_records('BOM')
|
test_records = frappe.get_test_records('BOM')
|
||||||
|
|
||||||
class TestBOM(unittest.TestCase):
|
class TestBOM(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if not frappe.get_value('Item', '_Test Item'):
|
if not frappe.get_value('Item', '_Test Item'):
|
||||||
make_test_records('Item')
|
make_test_records('Item')
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"col_break1",
|
"col_break1",
|
||||||
"workstation",
|
"workstation",
|
||||||
"time_in_mins",
|
"time_in_mins",
|
||||||
|
"fixed_time",
|
||||||
"costing_section",
|
"costing_section",
|
||||||
"hour_rate",
|
"hour_rate",
|
||||||
"base_hour_rate",
|
"base_hour_rate",
|
||||||
@@ -79,6 +80,14 @@
|
|||||||
"oldfieldtype": "Currency",
|
"oldfieldtype": "Currency",
|
||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"default": "0",
|
||||||
|
"description": "Operation time does not depend on quantity to produce",
|
||||||
|
"fieldname": "fixed_time",
|
||||||
|
"fieldtype": "Check",
|
||||||
|
"in_list_view": 1,
|
||||||
|
"label": "Fixed Time"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"fieldname": "operating_cost",
|
"fieldname": "operating_cost",
|
||||||
"fieldtype": "Currency",
|
"fieldtype": "Currency",
|
||||||
@@ -177,12 +186,13 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-09-13 16:45:01.092868",
|
"modified": "2021-12-15 03:00:00.473173",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Manufacturing",
|
"module": "Manufacturing",
|
||||||
"name": "BOM Operation",
|
"name": "BOM Operation",
|
||||||
"owner": "Administrator",
|
"owner": "Administrator",
|
||||||
"permissions": [],
|
"permissions": [],
|
||||||
"sort_field": "modified",
|
"sort_field": "modified",
|
||||||
"sort_order": "DESC"
|
"sort_order": "DESC",
|
||||||
|
"states": []
|
||||||
}
|
}
|
||||||
@@ -1,19 +1,16 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
|
||||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||||
from erpnext.stock.doctype.item.test_item import create_item
|
from erpnext.stock.doctype.item.test_item import create_item
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_records = frappe.get_test_records('BOM')
|
test_records = frappe.get_test_records('BOM')
|
||||||
|
|
||||||
class TestBOMUpdateTool(unittest.TestCase):
|
class TestBOMUpdateTool(ERPNextTestCase):
|
||||||
def test_replace_bom(self):
|
def test_replace_bom(self):
|
||||||
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
current_bom = "BOM-_Test Item Home Desktop Manufactured-001"
|
||||||
|
|
||||||
|
|||||||
@@ -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.doc.items || !frm.doc.items.length || frm.doc.for_quantity == frm.doc.transferred_qty)) {
|
||||||
frm.trigger("prepare_timer_buttons");
|
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) {
|
setup_corrective_job_card: function(frm) {
|
||||||
|
|||||||
@@ -4,10 +4,17 @@ from frappe import _
|
|||||||
def get_data():
|
def get_data():
|
||||||
return {
|
return {
|
||||||
'fieldname': 'job_card',
|
'fieldname': 'job_card',
|
||||||
|
'non_standard_fieldnames': {
|
||||||
|
'Quality Inspection': 'reference_name'
|
||||||
|
},
|
||||||
'transactions': [
|
'transactions': [
|
||||||
{
|
{
|
||||||
'label': _('Transactions'),
|
'label': _('Transactions'),
|
||||||
'items': ['Material Request', 'Stock Entry']
|
'items': ['Material Request', 'Stock Entry']
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'label': _('Reference'),
|
||||||
|
'items': ['Quality Inspection']
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import random_string
|
from frappe.utils import random_string
|
||||||
@@ -12,9 +11,10 @@ from erpnext.manufacturing.doctype.job_card.job_card import (
|
|||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
from erpnext.manufacturing.doctype.workstation.test_workstation import make_workstation
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestJobCard(unittest.TestCase):
|
class TestJobCard(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
make_bom_for_jc_tests()
|
make_bom_for_jc_tests()
|
||||||
|
|
||||||
@@ -329,4 +329,4 @@ def make_bom_for_jc_tests():
|
|||||||
bom.rm_cost_as_per = "Valuation Rate"
|
bom.rm_cost_as_per = "Valuation Rate"
|
||||||
bom.items[0].uom = "_Test UOM 1"
|
bom.items[0].uom = "_Test UOM 1"
|
||||||
bom.items[0].conversion_factor = 5
|
bom.items[0].conversion_factor = 5
|
||||||
bom.insert()
|
bom.insert()
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_to_date, flt, now_datetime, nowdate
|
from frappe.utils import add_to_date, flt, now_datetime, nowdate
|
||||||
|
|
||||||
@@ -17,9 +14,10 @@ from erpnext.stock.doctype.item.test_item import create_item
|
|||||||
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
|
||||||
create_stock_reconciliation,
|
create_stock_reconciliation,
|
||||||
)
|
)
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestProductionPlan(unittest.TestCase):
|
class TestProductionPlan(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
for item in ['Test Production Item 1', 'Subassembly Item 1',
|
for item in ['Test Production Item 1', 'Subassembly Item 1',
|
||||||
'Raw Material Item 1', 'Raw Material Item 2']:
|
'Raw Material Item 1', 'Raw Material Item 2']:
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
|
|
||||||
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
from erpnext.manufacturing.doctype.job_card.job_card import OperationSequenceError
|
||||||
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestRouting(unittest.TestCase):
|
class TestRouting(ERPNextTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
cls.item_code = "Test Routing Item - A"
|
cls.item_code = "Test Routing Item - A"
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_months, cint, flt, now, today
|
from frappe.utils import add_months, cint, flt, now, today
|
||||||
@@ -29,6 +28,9 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
self.warehouse = '_Test Warehouse 2 - _TC'
|
self.warehouse = '_Test Warehouse 2 - _TC'
|
||||||
self.item = '_Test Item'
|
self.item = '_Test Item'
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
frappe.db.rollback()
|
||||||
|
|
||||||
def check_planned_qty(self):
|
def check_planned_qty(self):
|
||||||
|
|
||||||
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
|
planned0 = frappe.db.get_value("Bin", {"item_code": "_Test FG Item",
|
||||||
@@ -92,7 +94,7 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
|
|
||||||
def test_reserved_qty_for_partial_completion(self):
|
def test_reserved_qty_for_partial_completion(self):
|
||||||
item = "_Test Item"
|
item = "_Test Item"
|
||||||
warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
|
warehouse = "_Test Warehouse - _TC"
|
||||||
|
|
||||||
bin1_at_start = get_bin(item, warehouse)
|
bin1_at_start = get_bin(item, warehouse)
|
||||||
|
|
||||||
@@ -844,6 +846,45 @@ class TestWorkOrder(ERPNextTestCase):
|
|||||||
close_work_order(wo_order, "Closed")
|
close_work_order(wo_order, "Closed")
|
||||||
self.assertEqual(wo_order.get('status'), "Closed")
|
self.assertEqual(wo_order.get('status'), "Closed")
|
||||||
|
|
||||||
|
def test_fix_time_operations(self):
|
||||||
|
bom = frappe.get_doc({
|
||||||
|
"doctype": "BOM",
|
||||||
|
"item": "_Test FG Item 2",
|
||||||
|
"is_active": 1,
|
||||||
|
"is_default": 1,
|
||||||
|
"quantity": 1.0,
|
||||||
|
"with_operations": 1,
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"operation": "_Test Operation 1",
|
||||||
|
"description": "_Test",
|
||||||
|
"workstation": "_Test Workstation 1",
|
||||||
|
"time_in_mins": 60,
|
||||||
|
"operating_cost": 140,
|
||||||
|
"fixed_time": 1
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"amount": 5000.0,
|
||||||
|
"doctype": "BOM Item",
|
||||||
|
"item_code": "_Test Item",
|
||||||
|
"parentfield": "items",
|
||||||
|
"qty": 1.0,
|
||||||
|
"rate": 5000.0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
bom.save()
|
||||||
|
bom.submit()
|
||||||
|
|
||||||
|
|
||||||
|
wo1 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=1, skip_transfer=1, do_not_submit=1)
|
||||||
|
wo2 = make_wo_order_test_record(item=bom.item, bom_no=bom.name, qty=2, skip_transfer=1, do_not_submit=1)
|
||||||
|
|
||||||
|
self.assertEqual(wo1.operations[0].time_in_mins, wo2.operations[0].time_in_mins)
|
||||||
|
|
||||||
|
|
||||||
def update_job_card(job_card):
|
def update_job_card(job_card):
|
||||||
job_card_doc = frappe.get_doc('Job Card', job_card)
|
job_card_doc = frappe.get_doc('Job Card', job_card)
|
||||||
job_card_doc.set('scrap_items', [
|
job_card_doc.set('scrap_items', [
|
||||||
|
|||||||
@@ -505,16 +505,19 @@ class WorkOrder(Document):
|
|||||||
"""Fetch operations from BOM and set in 'Work Order'"""
|
"""Fetch operations from BOM and set in 'Work Order'"""
|
||||||
|
|
||||||
def _get_operations(bom_no, qty=1):
|
def _get_operations(bom_no, qty=1):
|
||||||
return frappe.db.sql(
|
data = frappe.get_all("BOM Operation",
|
||||||
f"""select
|
filters={"parent": bom_no},
|
||||||
operation, description, workstation, idx,
|
fields=["operation", "description", "workstation", "idx",
|
||||||
base_hour_rate as hour_rate, time_in_mins * {qty} as time_in_mins,
|
"base_hour_rate as hour_rate", "time_in_mins", "parent as bom",
|
||||||
"Pending" as status, parent as bom, batch_size, sequence_id
|
"batch_size", "sequence_id", "fixed_time"],
|
||||||
from
|
order_by="idx")
|
||||||
`tabBOM Operation`
|
|
||||||
where
|
for d in data:
|
||||||
parent = %s order by idx
|
if not d.fixed_time:
|
||||||
""", bom_no, as_dict=1)
|
d.time_in_mins = flt(d.time_in_mins) * flt(qty)
|
||||||
|
d.status = "Pending"
|
||||||
|
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
self.set('operations', [])
|
self.set('operations', [])
|
||||||
@@ -542,7 +545,8 @@ class WorkOrder(Document):
|
|||||||
|
|
||||||
def calculate_time(self):
|
def calculate_time(self):
|
||||||
for d in self.get("operations"):
|
for d in self.get("operations"):
|
||||||
d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
|
if not d.fixed_time:
|
||||||
|
d.time_in_mins = flt(d.time_in_mins) * (flt(self.qty) / flt(d.batch_size))
|
||||||
|
|
||||||
self.calculate_operating_cost()
|
self.calculate_operating_cost()
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,5 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
|
||||||
# See license.txt
|
# See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
|
|
||||||
@@ -13,12 +10,13 @@ from erpnext.manufacturing.doctype.workstation.workstation import (
|
|||||||
WorkstationHolidayError,
|
WorkstationHolidayError,
|
||||||
check_if_within_operating_hours,
|
check_if_within_operating_hours,
|
||||||
)
|
)
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_dependencies = ["Warehouse"]
|
test_dependencies = ["Warehouse"]
|
||||||
test_records = frappe.get_test_records('Workstation')
|
test_records = frappe.get_test_records('Workstation')
|
||||||
make_test_records('Workstation')
|
make_test_records('Workstation')
|
||||||
|
|
||||||
class TestWorkstation(unittest.TestCase):
|
class TestWorkstation(ERPNextTestCase):
|
||||||
def test_validate_timings(self):
|
def test_validate_timings(self):
|
||||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
|
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 11:00:00", "2013-02-02 19:00:00")
|
||||||
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
|
check_if_within_operating_hours("_Test Workstation 1", "Operation 1", "2013-02-02 10:00:00", "2013-02-02 20:00:00")
|
||||||
|
|||||||
@@ -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.set_default_payroll_based_on
|
||||||
erpnext.patches.v12_0.repost_stock_ledger_entries_for_target_warehouse
|
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.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.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.fix_quotation_expired_status
|
||||||
erpnext.patches.v12_0.rename_pos_closing_doctype
|
erpnext.patches.v12_0.rename_pos_closing_doctype
|
||||||
@@ -287,7 +288,6 @@ execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Catego
|
|||||||
erpnext.patches.v14_0.delete_einvoicing_doctypes
|
erpnext.patches.v14_0.delete_einvoicing_doctypes
|
||||||
erpnext.patches.v13_0.custom_fields_for_taxjar_integration #08-11-2021
|
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.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.v13_0.create_gst_payment_entry_fields #27-11-2021
|
||||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||||
erpnext.patches.v13_0.fix_invoice_statuses
|
erpnext.patches.v13_0.fix_invoice_statuses
|
||||||
@@ -314,4 +314,8 @@ erpnext.patches.v13_0.create_pan_field_for_india #2
|
|||||||
erpnext.patches.v14_0.delete_hub_doctypes
|
erpnext.patches.v14_0.delete_hub_doctypes
|
||||||
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
erpnext.patches.v13_0.create_ksa_vat_custom_fields
|
||||||
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
erpnext.patches.v13_0.update_maintenance_schedule_field_in_visit
|
||||||
|
erpnext.patches.v14_0.rename_ongoing_status_in_sla_documents
|
||||||
erpnext.patches.v14_0.migrate_crm_settings
|
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
|
||||||
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal file
16
erpnext/patches/v13_0/disable_ksa_print_format_for_others.py
Normal 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)
|
||||||
32
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal file
32
erpnext/patches/v13_0/rename_ksa_qr_field.py
Normal 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")
|
||||||
@@ -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()
|
||||||
@@ -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')
|
||||||
@@ -940,10 +940,12 @@ class SalarySlip(TransactionBase):
|
|||||||
|
|
||||||
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
|
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
|
||||||
amount, additional_amount = row.amount, row.additional_amount
|
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
|
if (self.salary_structure and
|
||||||
cint(row.depends_on_payment_days) and cint(self.total_working_days)
|
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 (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
|
getdate(self.start_date) < joining_date or
|
||||||
(relieving_date and getdate(self.end_date) > relieving_date)
|
(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)
|
amount = flt((flt(row.default_amount) * flt(self.payment_days)
|
||||||
/ cint(self.total_working_days)), row.precision("amount")) + additional_amount
|
/ 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
|
amount, additional_amount = 0, 0
|
||||||
elif not row.amount:
|
elif not row.amount:
|
||||||
amount = flt(row.default_amount) + flt(row.additional_amount)
|
amount = flt(row.default_amount) + flt(row.additional_amount)
|
||||||
|
|||||||
@@ -134,6 +134,57 @@ class TestSalarySlip(unittest.TestCase):
|
|||||||
|
|
||||||
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Leave")
|
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):
|
def test_component_amount_dependent_on_another_payment_days_based_component(self):
|
||||||
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
from erpnext.hr.doctype.attendance.attendance import mark_attendance
|
||||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
from erpnext.payroll.doctype.salary_structure.test_salary_structure import (
|
||||||
|
|||||||
@@ -34,10 +34,6 @@ class TestTimesheet(unittest.TestCase):
|
|||||||
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
|
||||||
frappe.db.sql("delete from `tab%s`" % dt)
|
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):
|
def test_timesheet_billing_amount(self):
|
||||||
emp = make_employee("test_employee_6@salary.com")
|
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"
|
salary_structure_name = "Timesheet Salary Structure Test"
|
||||||
frequency = "Monthly"
|
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 = make_salary_structure(salary_structure_name, frequency, company=company, dont_submit=True)
|
||||||
salary_structure.salary_component = "Timesheet Component"
|
salary_structure.salary_component = "Timesheet Component"
|
||||||
salary_structure.salary_slip_based_on_timesheet = 1
|
salary_structure.salary_slip_based_on_timesheet = 1
|
||||||
|
|||||||
@@ -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: () => {
|
proceed_save_with_reminders_frequency_change: () => {
|
||||||
frappe.ui.hide_open_dialog();
|
frappe.ui.hide_open_dialog();
|
||||||
|
|
||||||
@@ -831,7 +835,7 @@ $(document).on('app_ready', function() {
|
|||||||
|
|
||||||
refresh: function(frm) {
|
refresh: function(frm) {
|
||||||
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
|
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({
|
frappe.call({
|
||||||
'method': 'frappe.client.get',
|
'method': 'frappe.client.get',
|
||||||
args: {
|
args: {
|
||||||
@@ -884,9 +888,11 @@ $(document).on('app_ready', function() {
|
|||||||
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
||||||
frm.dashboard.clear_headline();
|
frm.dashboard.clear_headline();
|
||||||
|
|
||||||
let time_to_respond = get_status(frm.doc.response_by_variance);
|
let time_to_respond;
|
||||||
if (!frm.doc.first_responded_on && frm.doc.agreement_status === 'Ongoing') {
|
if (!frm.doc.first_responded_on) {
|
||||||
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
|
time_to_respond = get_time_left(frm.doc.response_by, frm.doc.agreement_status);
|
||||||
|
} else {
|
||||||
|
time_to_respond = get_status(frm.doc.response_by, frm.doc.first_responded_on);
|
||||||
}
|
}
|
||||||
|
|
||||||
let alert = `
|
let alert = `
|
||||||
@@ -899,9 +905,11 @@ function set_time_to_resolve_and_response(frm, apply_sla_for_resolution) {
|
|||||||
|
|
||||||
|
|
||||||
if (apply_sla_for_resolution) {
|
if (apply_sla_for_resolution) {
|
||||||
let time_to_resolve = get_status(frm.doc.resolution_by_variance);
|
let time_to_resolve;
|
||||||
if (!frm.doc.resolution_date && frm.doc.agreement_status === 'Ongoing') {
|
if (!frm.doc.resolution_date) {
|
||||||
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
|
time_to_resolve = get_time_left(frm.doc.resolution_by, frm.doc.agreement_status);
|
||||||
|
} else {
|
||||||
|
time_to_resolve = get_status(frm.doc.resolution_by, frm.doc.resolution_date);
|
||||||
}
|
}
|
||||||
|
|
||||||
alert += `
|
alert += `
|
||||||
@@ -924,8 +932,9 @@ function get_time_left(timestamp, agreement_status) {
|
|||||||
return {'diff_display': diff_display, 'indicator': indicator};
|
return {'diff_display': diff_display, 'indicator': indicator};
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_status(variance) {
|
function get_status(expected, actual) {
|
||||||
if (variance > 0) {
|
const time_left = moment(expected).diff(moment(actual));
|
||||||
|
if (time_left >= 0) {
|
||||||
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
|
return {'diff_display': 'Fulfilled', 'indicator': 'green'};
|
||||||
} else {
|
} else {
|
||||||
return {'diff_display': 'Failed', 'indicator': 'red'};
|
return {'diff_display': 'Failed', 'indicator': 'red'};
|
||||||
|
|||||||
@@ -213,10 +213,11 @@ def get_regional_address_details(party_details, doctype, company):
|
|||||||
tax_template_by_category = 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 tax_template_by_category:
|
if tax_template_by_category:
|
||||||
party_details.get['taxes_and_charges'] = tax_template_by_category
|
party_details['taxes_and_charges'] = tax_template_by_category
|
||||||
return
|
return
|
||||||
|
|
||||||
if not party_details.place_of_supply: return party_details
|
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
|
if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin
|
||||||
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
|
and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice",
|
||||||
|
|||||||
@@ -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
@@ -114,9 +114,11 @@ def get_items(filters):
|
|||||||
|
|
||||||
items = frappe.db.sql("""
|
items = frappe.db.sql("""
|
||||||
select
|
select
|
||||||
`tabSales Invoice Item`.name, `tabSales Invoice Item`.base_price_list_rate,
|
`tabSales Invoice Item`.gst_hsn_code,
|
||||||
`tabSales Invoice Item`.gst_hsn_code, `tabSales Invoice Item`.stock_qty,
|
`tabSales Invoice Item`.stock_uom,
|
||||||
`tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_amount,
|
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,
|
`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code,
|
||||||
`tabGST HSN Code`.description
|
`tabGST HSN Code`.description
|
||||||
from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code`
|
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`.docstatus = 1
|
||||||
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
and `tabSales Invoice Item`.gst_hsn_code is not NULL
|
||||||
and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s
|
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)
|
""" % (conditions, match_conditions), filters, as_dict=1)
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.permissions import add_permission, update_permission_property
|
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 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
|
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||||
|
|
||||||
@@ -13,6 +13,16 @@ def setup(company=None, patch=True):
|
|||||||
add_permissions()
|
add_permissions()
|
||||||
make_custom_fields()
|
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():
|
def add_permissions():
|
||||||
"""Add Permissions for KSA VAT Setting."""
|
"""Add Permissions for KSA VAT Setting."""
|
||||||
add_permission('KSA VAT Setting', 'All', 0)
|
add_permission('KSA VAT Setting', 'All', 0)
|
||||||
@@ -33,8 +43,16 @@ def make_custom_fields():
|
|||||||
custom_fields = {
|
custom_fields = {
|
||||||
'Sales Invoice': [
|
'Sales Invoice': [
|
||||||
dict(
|
dict(
|
||||||
fieldname='qr_code',
|
fieldname='ksa_einv_qr',
|
||||||
label='QR Code',
|
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',
|
fieldtype='Attach Image',
|
||||||
read_only=1, no_copy=1, hidden=1
|
read_only=1, no_copy=1, hidden=1
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -4,144 +4,146 @@ from base64 import b64encode
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
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 frappe.utils.data import add_to_date, get_time, getdate
|
||||||
from pyqrcode import create as qr_create
|
from pyqrcode import create as qr_create
|
||||||
|
|
||||||
from erpnext import get_region
|
from erpnext import get_region
|
||||||
|
|
||||||
|
|
||||||
def create_qr_code(doc, method):
|
def create_qr_code(doc, method=None):
|
||||||
"""Create QR Code after inserting Sales Inv
|
|
||||||
"""
|
|
||||||
|
|
||||||
region = get_region(doc.company)
|
region = get_region(doc.company)
|
||||||
if region not in ['Saudi Arabia']:
|
if region not in ['Saudi Arabia']:
|
||||||
return
|
return
|
||||||
|
|
||||||
# if QR Code field not present, do nothing
|
# if QR Code field not present, create it. Invoices without QR are invalid as per law.
|
||||||
if not hasattr(doc, 'qr_code'):
|
if not hasattr(doc, 'ksa_einv_qr'):
|
||||||
return
|
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
|
# 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}):
|
if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
|
||||||
return
|
return
|
||||||
|
|
||||||
meta = frappe.get_meta('Sales Invoice')
|
meta = frappe.get_meta(doc.doctype)
|
||||||
|
|
||||||
for field in meta.get_image_fields():
|
if "ksa_einv_qr" in [d.fieldname for d in meta.get_image_fields()]:
|
||||||
if field.fieldname == 'qr_code':
|
''' TLV conversion for
|
||||||
''' TLV conversion for
|
1. Seller's Name
|
||||||
1. Seller's Name
|
2. VAT Number
|
||||||
2. VAT Number
|
3. Time Stamp
|
||||||
3. Time Stamp
|
4. Invoice Amount
|
||||||
4. Invoice Amount
|
5. VAT Amount
|
||||||
5. VAT Amount
|
'''
|
||||||
'''
|
tlv_array = []
|
||||||
tlv_array = []
|
# Sellers Name
|
||||||
# Sellers Name
|
|
||||||
|
|
||||||
seller_name = frappe.db.get_value(
|
seller_name = frappe.db.get_value(
|
||||||
'Company',
|
'Company',
|
||||||
doc.company,
|
doc.company,
|
||||||
'company_name_in_arabic')
|
'company_name_in_arabic')
|
||||||
|
|
||||||
if not seller_name:
|
if not seller_name:
|
||||||
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
frappe.throw(_('Arabic name missing for {} in the company document').format(doc.company))
|
||||||
|
|
||||||
tag = bytes([1]).hex()
|
tag = bytes([1]).hex()
|
||||||
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
length = bytes([len(seller_name.encode('utf-8'))]).hex()
|
||||||
value = seller_name.encode('utf-8').hex()
|
value = seller_name.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# VAT Number
|
# VAT Number
|
||||||
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
tax_id = frappe.db.get_value('Company', doc.company, 'tax_id')
|
||||||
if not tax_id:
|
if not tax_id:
|
||||||
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
frappe.throw(_('Tax ID missing for {} in the company document').format(doc.company))
|
||||||
|
|
||||||
tag = bytes([2]).hex()
|
tag = bytes([2]).hex()
|
||||||
length = bytes([len(tax_id)]).hex()
|
length = bytes([len(tax_id)]).hex()
|
||||||
value = tax_id.encode('utf-8').hex()
|
value = tax_id.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# Time Stamp
|
# Time Stamp
|
||||||
posting_date = getdate(doc.posting_date)
|
posting_date = getdate(doc.posting_date)
|
||||||
time = get_time(doc.posting_time)
|
time = get_time(doc.posting_time)
|
||||||
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
seconds = time.hour * 60 * 60 + time.minute * 60 + time.second
|
||||||
time_stamp = add_to_date(posting_date, seconds=seconds)
|
time_stamp = add_to_date(posting_date, seconds=seconds)
|
||||||
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
time_stamp = time_stamp.strftime('%Y-%m-%dT%H:%M:%SZ')
|
||||||
|
|
||||||
tag = bytes([3]).hex()
|
tag = bytes([3]).hex()
|
||||||
length = bytes([len(time_stamp)]).hex()
|
length = bytes([len(time_stamp)]).hex()
|
||||||
value = time_stamp.encode('utf-8').hex()
|
value = time_stamp.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# Invoice Amount
|
# Invoice Amount
|
||||||
invoice_amount = str(doc.grand_total)
|
invoice_amount = str(doc.grand_total)
|
||||||
tag = bytes([4]).hex()
|
tag = bytes([4]).hex()
|
||||||
length = bytes([len(invoice_amount)]).hex()
|
length = bytes([len(invoice_amount)]).hex()
|
||||||
value = invoice_amount.encode('utf-8').hex()
|
value = invoice_amount.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# VAT Amount
|
# VAT Amount
|
||||||
vat_amount = str(doc.total_taxes_and_charges)
|
vat_amount = str(doc.total_taxes_and_charges)
|
||||||
|
|
||||||
tag = bytes([5]).hex()
|
tag = bytes([5]).hex()
|
||||||
length = bytes([len(vat_amount)]).hex()
|
length = bytes([len(vat_amount)]).hex()
|
||||||
value = vat_amount.encode('utf-8').hex()
|
value = vat_amount.encode('utf-8').hex()
|
||||||
tlv_array.append(''.join([tag, length, value]))
|
tlv_array.append(''.join([tag, length, value]))
|
||||||
|
|
||||||
# Joining bytes into one
|
# Joining bytes into one
|
||||||
tlv_buff = ''.join(tlv_array)
|
tlv_buff = ''.join(tlv_array)
|
||||||
|
|
||||||
# base64 conversion for QR Code
|
# base64 conversion for QR Code
|
||||||
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
base64_string = b64encode(bytes.fromhex(tlv_buff)).decode()
|
||||||
|
|
||||||
qr_image = io.BytesIO()
|
qr_image = io.BytesIO()
|
||||||
url = qr_create(base64_string, error='L')
|
url = qr_create(base64_string, error='L')
|
||||||
url.png(qr_image, scale=2, quiet_zone=1)
|
url.png(qr_image, scale=2, quiet_zone=1)
|
||||||
|
|
||||||
name = frappe.generate_hash(doc.name, 5)
|
name = frappe.generate_hash(doc.name, 5)
|
||||||
|
|
||||||
# making file
|
# making file
|
||||||
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
filename = f"QRCode-{name}.png".replace(os.path.sep, "__")
|
||||||
_file = frappe.get_doc({
|
_file = frappe.get_doc({
|
||||||
"doctype": "File",
|
"doctype": "File",
|
||||||
"file_name": filename,
|
"file_name": filename,
|
||||||
"is_private": 0,
|
"is_private": 0,
|
||||||
"content": qr_image.getvalue(),
|
"content": qr_image.getvalue(),
|
||||||
"attached_to_doctype": doc.get("doctype"),
|
"attached_to_doctype": doc.get("doctype"),
|
||||||
"attached_to_name": doc.get("name"),
|
"attached_to_name": doc.get("name"),
|
||||||
"attached_to_field": "qr_code"
|
"attached_to_field": "ksa_einv_qr"
|
||||||
})
|
})
|
||||||
|
|
||||||
_file.save()
|
_file.save()
|
||||||
|
|
||||||
# assigning to document
|
# assigning to document
|
||||||
doc.db_set('qr_code', _file.file_url)
|
doc.db_set('ksa_einv_qr', _file.file_url)
|
||||||
doc.notify_update()
|
doc.notify_update()
|
||||||
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def delete_qr_code_file(doc, method):
|
def delete_qr_code_file(doc, method=None):
|
||||||
"""Delete QR Code on deleted sales invoice"""
|
|
||||||
|
|
||||||
region = get_region(doc.company)
|
region = get_region(doc.company)
|
||||||
if region not in ['Saudi Arabia']:
|
if region not in ['Saudi Arabia']:
|
||||||
return
|
return
|
||||||
|
|
||||||
if hasattr(doc, 'qr_code'):
|
if hasattr(doc, 'ksa_einv_qr'):
|
||||||
if doc.get('qr_code'):
|
if doc.get('ksa_einv_qr'):
|
||||||
file_doc = frappe.get_list('File', {
|
file_doc = frappe.get_list('File', {
|
||||||
'file_url': doc.get('qr_code')
|
'file_url': doc.get('ksa_einv_qr')
|
||||||
})
|
})
|
||||||
if len(file_doc):
|
if len(file_doc):
|
||||||
frappe.delete_doc('File', file_doc[0].name)
|
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':
|
if doc.country != 'Saudi Arabia':
|
||||||
return
|
return
|
||||||
|
|
||||||
settings_doc = frappe.get_doc('KSA VAT Setting', {'company': doc.name})
|
if frappe.db.exists('KSA VAT Setting', doc.name):
|
||||||
settings_doc.delete()
|
frappe.delete_doc('KSA VAT Setting', doc.name)
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.test_runner import make_test_records
|
from frappe.test_runner import make_test_records
|
||||||
from frappe.utils import flt
|
from frappe.utils import flt
|
||||||
@@ -11,7 +9,7 @@ from frappe.utils import flt
|
|||||||
from erpnext.accounts.party import get_due_date
|
from erpnext.accounts.party import get_due_date
|
||||||
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
from erpnext.exceptions import PartyDisabled, PartyFrozen
|
||||||
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
|
from erpnext.selling.doctype.customer.customer import get_credit_limit, get_customer_outstanding
|
||||||
from erpnext.tests.utils import create_test_contact_and_address
|
from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
|
||||||
|
|
||||||
test_ignore = ["Price List"]
|
test_ignore = ["Price List"]
|
||||||
test_dependencies = ['Payment Term', 'Payment Terms Template']
|
test_dependencies = ['Payment Term', 'Payment Terms Template']
|
||||||
@@ -19,7 +17,7 @@ test_records = frappe.get_test_records('Customer')
|
|||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TestCustomer(unittest.TestCase):
|
class TestCustomer(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
if not frappe.get_value('Item', '_Test Item'):
|
if not frappe.get_value('Item', '_Test Item'):
|
||||||
make_test_records('Item')
|
make_test_records('Item')
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import unittest
|
|||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.controllers.queries import item_query
|
from erpnext.controllers.queries import item_query
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_dependencies = ['Item', 'Customer', 'Supplier']
|
test_dependencies = ['Item', 'Customer', 'Supplier']
|
||||||
|
|
||||||
@@ -17,7 +18,7 @@ def create_party_specific_item(**args):
|
|||||||
psi.based_on_value = args.get('based_on_value')
|
psi.based_on_value = args.get('based_on_value')
|
||||||
psi.insert()
|
psi.insert()
|
||||||
|
|
||||||
class TestPartySpecificItem(unittest.TestCase):
|
class TestPartySpecificItem(ERPNextTestCase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.customer = frappe.get_last_doc("Customer")
|
self.customer = frappe.get_last_doc("Customer")
|
||||||
self.supplier = frappe.get_last_doc("Supplier")
|
self.supplier = frappe.get_last_doc("Supplier")
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
from frappe.utils import add_days, add_months, flt, getdate, nowdate
|
||||||
|
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
test_dependencies = ["Product Bundle"]
|
test_dependencies = ["Product Bundle"]
|
||||||
|
|
||||||
|
|
||||||
class TestQuotation(unittest.TestCase):
|
class TestQuotation(ERPNextTestCase):
|
||||||
def test_make_quotation_without_terms(self):
|
def test_make_quotation_without_terms(self):
|
||||||
quotation = make_quotation(do_not_save=1)
|
quotation = make_quotation(do_not_save=1)
|
||||||
self.assertFalse(quotation.get('payment_schedule'))
|
self.assertFalse(quotation.get('payment_schedule'))
|
||||||
|
|||||||
@@ -63,6 +63,8 @@ class SalesOrder(SellingController):
|
|||||||
if not self.billing_status: self.billing_status = 'Not Billed'
|
if not self.billing_status: self.billing_status = 'Not Billed'
|
||||||
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
if not self.delivery_status: self.delivery_status = 'Not Delivered'
|
||||||
|
|
||||||
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_po(self):
|
def validate_po(self):
|
||||||
# validate p.o date v/s delivery date
|
# validate p.o date v/s delivery date
|
||||||
if self.po_date and not self.skip_delivery_note:
|
if self.po_date and not self.skip_delivery_note:
|
||||||
@@ -978,6 +980,7 @@ def make_work_orders(items, sales_order, company, project=None):
|
|||||||
description=i['description']
|
description=i['description']
|
||||||
)).insert()
|
)).insert()
|
||||||
work_order.set_work_order_operations()
|
work_order.set_work_order_operations()
|
||||||
|
work_order.flags.ignore_mandatory = True
|
||||||
work_order.save()
|
work_order.save()
|
||||||
out.append(work_order)
|
out.append(work_order)
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
# License: GNU General Public License v3. See license.txt
|
# License: GNU General Public License v3. See license.txt
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
import frappe.permissions
|
import frappe.permissions
|
||||||
@@ -28,12 +27,14 @@ from erpnext.selling.doctype.sales_order.sales_order import (
|
|||||||
)
|
)
|
||||||
from erpnext.stock.doctype.item.test_item import make_item
|
from erpnext.stock.doctype.item.test_item import make_item
|
||||||
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestSalesOrder(unittest.TestCase):
|
class TestSalesOrder(ERPNextTestCase):
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def setUpClass(cls):
|
def setUpClass(cls):
|
||||||
|
super().setUpClass()
|
||||||
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
|
cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
|
||||||
"unlink_advance_payment_on_cancelation_of_order"))
|
"unlink_advance_payment_on_cancelation_of_order"))
|
||||||
|
|
||||||
@@ -42,6 +43,7 @@ class TestSalesOrder(unittest.TestCase):
|
|||||||
# reset config to previous state
|
# reset config to previous state
|
||||||
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
frappe.db.set_value("Accounts Settings", "Accounts Settings",
|
||||||
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
|
"unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
|
||||||
|
super().tearDownClass()
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
frappe.set_user("Administrator")
|
frappe.set_user("Administrator")
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
from frappe.utils import add_months, nowdate
|
from frappe.utils import add_months, nowdate
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
|
from erpnext.selling.doctype.sales_order.sales_order import make_material_request
|
||||||
@@ -11,9 +9,10 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde
|
|||||||
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
|
from erpnext.selling.report.pending_so_items_for_purchase_request.pending_so_items_for_purchase_request import (
|
||||||
execute,
|
execute,
|
||||||
)
|
)
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestPendingSOItemsForPurchaseRequest(unittest.TestCase):
|
class TestPendingSOItemsForPurchaseRequest(ERPNextTestCase):
|
||||||
def test_result_for_partial_material_request(self):
|
def test_result_for_partial_material_request(self):
|
||||||
so = make_sales_order()
|
so = make_sales_order()
|
||||||
mr=make_material_request(so.name)
|
mr=make_material_request(so.name)
|
||||||
|
|||||||
@@ -2,15 +2,14 @@
|
|||||||
# For license information, please see license.txt
|
# For license information, please see license.txt
|
||||||
|
|
||||||
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
|
||||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||||
from erpnext.selling.report.sales_analytics.sales_analytics import execute
|
from erpnext.selling.report.sales_analytics.sales_analytics import execute
|
||||||
|
from erpnext.tests.utils import ERPNextTestCase
|
||||||
|
|
||||||
|
|
||||||
class TestAnalytics(unittest.TestCase):
|
class TestAnalytics(ERPNextTestCase):
|
||||||
def test_sales_analytics(self):
|
def test_sales_analytics(self):
|
||||||
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
frappe.db.sql("delete from `tabSales Order` where company='_Test Company 2'")
|
||||||
|
|
||||||
|
|||||||
@@ -68,6 +68,8 @@ def set_default_settings(args):
|
|||||||
|
|
||||||
hr_settings.send_interview_feedback_reminder = 1
|
hr_settings.send_interview_feedback_reminder = 1
|
||||||
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
|
hr_settings.feedback_reminder_notification_template = _("Interview Feedback Reminder")
|
||||||
|
|
||||||
|
hr_settings.exit_questionnaire_notification_template = _("Exit Questionnaire Notification")
|
||||||
hr_settings.save()
|
hr_settings.save()
|
||||||
|
|
||||||
def set_no_copy_fields_in_variant_settings():
|
def set_no_copy_fields_in_variant_settings():
|
||||||
|
|||||||
@@ -278,6 +278,11 @@ def install(country=None):
|
|||||||
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
|
records += [{'doctype': 'Email Template', 'name': _('Interview Feedback Reminder'), 'response': response,
|
||||||
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
|
'subject': _('Interview Feedback Reminder'), 'owner': frappe.session.user}]
|
||||||
|
|
||||||
|
response = frappe.read_file(os.path.join(base_path, 'exit_interview/exit_questionnaire_notification_template.html'))
|
||||||
|
|
||||||
|
records += [{'doctype': 'Email Template', 'name': _('Exit Questionnaire Notification'), 'response': response,
|
||||||
|
'subject': _('Exit Questionnaire Notification'), 'owner': frappe.session.user}]
|
||||||
|
|
||||||
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
|
base_path = frappe.get_app_path("erpnext", "stock", "doctype")
|
||||||
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
|
response = frappe.read_file(os.path.join(base_path, "delivery_trip/dispatch_notification_template.html"))
|
||||||
|
|
||||||
|
|||||||
@@ -43,9 +43,9 @@ class Bin(Document):
|
|||||||
frappe.qb
|
frappe.qb
|
||||||
.from_(wo)
|
.from_(wo)
|
||||||
.from_(wo_item)
|
.from_(wo_item)
|
||||||
.select(Case()
|
.select(Sum(Case()
|
||||||
.when(wo.skip_transfer == 0, Sum(wo_item.required_qty - wo_item.transferred_qty))
|
.when(wo.skip_transfer == 0, wo_item.required_qty - wo_item.transferred_qty)
|
||||||
.else_(Sum(wo_item.required_qty - wo_item.consumed_qty))
|
.else_(wo_item.required_qty - wo_item.consumed_qty))
|
||||||
)
|
)
|
||||||
.where(
|
.where(
|
||||||
(wo_item.item_code == self.item_code)
|
(wo_item.item_code == self.item_code)
|
||||||
|
|||||||
@@ -138,6 +138,7 @@ class DeliveryNote(SellingController):
|
|||||||
self.update_current_stock()
|
self.update_current_stock()
|
||||||
|
|
||||||
if not self.installation_status: self.installation_status = 'Not Installed'
|
if not self.installation_status: self.installation_status = 'Not Installed'
|
||||||
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
|
||||||
def validate_with_previous_doc(self):
|
def validate_with_previous_doc(self):
|
||||||
super(DeliveryNote, self).validate_with_previous_doc({
|
super(DeliveryNote, self).validate_with_previous_doc({
|
||||||
|
|||||||
@@ -361,8 +361,7 @@
|
|||||||
"fieldname": "valuation_method",
|
"fieldname": "valuation_method",
|
||||||
"fieldtype": "Select",
|
"fieldtype": "Select",
|
||||||
"label": "Valuation Method",
|
"label": "Valuation Method",
|
||||||
"options": "\nFIFO\nMoving Average",
|
"options": "\nFIFO\nMoving Average"
|
||||||
"set_only_once": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "is_stock_item",
|
"depends_on": "is_stock_item",
|
||||||
@@ -1035,7 +1034,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2021-12-03 08:32:03.869294",
|
"modified": "2021-12-14 04:13:16.857534",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -534,8 +534,6 @@ class TestItem(ERPNextTestCase):
|
|||||||
|
|
||||||
def test_index_creation(self):
|
def test_index_creation(self):
|
||||||
"check if index is getting created in db"
|
"check if index is getting created in db"
|
||||||
from erpnext.stock.doctype.item.item import on_doctype_update
|
|
||||||
on_doctype_update()
|
|
||||||
|
|
||||||
indices = frappe.db.sql("show index from tabItem", as_dict=1)
|
indices = frappe.db.sql("show index from tabItem", as_dict=1)
|
||||||
expected_columns = {"item_code", "item_name", "item_group", "route"}
|
expected_columns = {"item_code", "item_name", "item_group", "route"}
|
||||||
|
|||||||
@@ -80,6 +80,9 @@ class MaterialRequest(BuyingController):
|
|||||||
# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
|
# NOTE: Since Item BOM and FG quantities are combined, using current data, it cannot be validated
|
||||||
# Though the creation of Material Request from a Production Plan can be rethought to fix this
|
# Though the creation of Material Request from a Production Plan can be rethought to fix this
|
||||||
|
|
||||||
|
self.reset_default_field_value("set_warehouse", "items", "warehouse")
|
||||||
|
self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse")
|
||||||
|
|
||||||
def set_title(self):
|
def set_title(self):
|
||||||
'''Set title as comma separated list of items'''
|
'''Set title as comma separated list of items'''
|
||||||
if not self.title:
|
if not self.title:
|
||||||
|
|||||||
@@ -1,451 +1,140 @@
|
|||||||
{
|
{
|
||||||
"allow_copy": 0,
|
"actions": [],
|
||||||
"allow_import": 0,
|
"autoname": "hash",
|
||||||
"allow_rename": 0,
|
"creation": "2013-04-08 13:10:16",
|
||||||
"autoname": "hash",
|
"doctype": "DocType",
|
||||||
"beta": 0,
|
"document_type": "Document",
|
||||||
"creation": "2013-04-08 13:10:16",
|
"editable_grid": 1,
|
||||||
"custom": 0,
|
"engine": "InnoDB",
|
||||||
"docstatus": 0,
|
"field_order": [
|
||||||
"doctype": "DocType",
|
"item_code",
|
||||||
"document_type": "Document",
|
"column_break_2",
|
||||||
"editable_grid": 1,
|
"item_name",
|
||||||
"engine": "InnoDB",
|
"batch_no",
|
||||||
|
"desc_section",
|
||||||
|
"description",
|
||||||
|
"quantity_section",
|
||||||
|
"qty",
|
||||||
|
"net_weight",
|
||||||
|
"column_break_10",
|
||||||
|
"stock_uom",
|
||||||
|
"weight_uom",
|
||||||
|
"page_break",
|
||||||
|
"dn_detail"
|
||||||
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "item_code",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"in_global_search": 1,
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"fieldname": "item_code",
|
"label": "Item Code",
|
||||||
"fieldtype": "Link",
|
"options": "Item",
|
||||||
"hidden": 0,
|
"print_width": "100px",
|
||||||
"ignore_user_permissions": 0,
|
"reqd": 1,
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 1,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Code",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Item",
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "100px",
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "column_break_2",
|
||||||
"bold": 0,
|
"fieldtype": "Column Break"
|
||||||
"collapsible": 0,
|
},
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_2",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fetch_from": "item_code.item_name",
|
||||||
"bold": 0,
|
"fieldname": "item_name",
|
||||||
"collapsible": 0,
|
"fieldtype": "Data",
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"fieldname": "item_name",
|
"label": "Item Name",
|
||||||
"fieldtype": "Data",
|
"print_width": "200px",
|
||||||
"hidden": 0,
|
"read_only": 1,
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Item Name",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "item_code.item_name",
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "200px",
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "200px"
|
"width": "200px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "batch_no",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"label": "Batch No",
|
||||||
"columns": 0,
|
"options": "Batch"
|
||||||
"fieldname": "batch_no",
|
},
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Batch No",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "Batch",
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"collapsible": 1,
|
||||||
"bold": 0,
|
"fieldname": "desc_section",
|
||||||
"collapsible": 1,
|
"fieldtype": "Section Break",
|
||||||
"columns": 0,
|
"label": "Description"
|
||||||
"fieldname": "desc_section",
|
},
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Description",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "description",
|
||||||
"bold": 0,
|
"fieldtype": "Text Editor",
|
||||||
"collapsible": 0,
|
"label": "Description"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "description",
|
|
||||||
"fieldtype": "Text Editor",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Description",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "quantity_section",
|
||||||
"bold": 0,
|
"fieldtype": "Section Break",
|
||||||
"collapsible": 0,
|
"label": "Quantity"
|
||||||
"columns": 0,
|
},
|
||||||
"fieldname": "quantity_section",
|
|
||||||
"fieldtype": "Section Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Quantity",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "qty",
|
||||||
"bold": 0,
|
"fieldtype": "Float",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Quantity",
|
||||||
"fieldname": "qty",
|
"print_width": "100px",
|
||||||
"fieldtype": "Float",
|
"reqd": 1,
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Quantity",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "100px",
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 1,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "net_weight",
|
||||||
"bold": 0,
|
"fieldtype": "Float",
|
||||||
"collapsible": 0,
|
"in_list_view": 1,
|
||||||
"columns": 0,
|
"label": "Net Weight",
|
||||||
"fieldname": "net_weight",
|
"print_width": "100px",
|
||||||
"fieldtype": "Float",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Net Weight",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "100px",
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "column_break_10",
|
||||||
"bold": 0,
|
"fieldtype": "Column Break"
|
||||||
"collapsible": 0,
|
},
|
||||||
"columns": 0,
|
|
||||||
"fieldname": "column_break_10",
|
|
||||||
"fieldtype": "Column Break",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"precision": "",
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "stock_uom",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"label": "UOM",
|
||||||
"columns": 0,
|
"options": "UOM",
|
||||||
"fieldname": "stock_uom",
|
"print_width": "100px",
|
||||||
"fieldtype": "Link",
|
"read_only": 1,
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "UOM",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "UOM",
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "100px",
|
|
||||||
"read_only": 1,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "weight_uom",
|
||||||
"bold": 0,
|
"fieldtype": "Link",
|
||||||
"collapsible": 0,
|
"label": "Weight UOM",
|
||||||
"columns": 0,
|
"options": "UOM",
|
||||||
"fieldname": "weight_uom",
|
"print_width": "100px",
|
||||||
"fieldtype": "Link",
|
|
||||||
"hidden": 0,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 0,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Weight UOM",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"options": "UOM",
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"print_width": "100px",
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0,
|
|
||||||
"width": "100px"
|
"width": "100px"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"allow_on_submit": 1,
|
"allow_on_submit": 1,
|
||||||
"bold": 0,
|
"default": "0",
|
||||||
"collapsible": 0,
|
"fieldname": "page_break",
|
||||||
"columns": 0,
|
"fieldtype": "Check",
|
||||||
"fieldname": "page_break",
|
"in_list_view": 1,
|
||||||
"fieldtype": "Check",
|
"label": "Page Break"
|
||||||
"hidden": 0,
|
},
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "Page Break",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"allow_on_submit": 0,
|
"fieldname": "dn_detail",
|
||||||
"bold": 0,
|
"fieldtype": "Data",
|
||||||
"collapsible": 0,
|
"hidden": 1,
|
||||||
"columns": 0,
|
"in_list_view": 1,
|
||||||
"fieldname": "dn_detail",
|
"label": "DN Detail"
|
||||||
"fieldtype": "Data",
|
|
||||||
"hidden": 1,
|
|
||||||
"ignore_user_permissions": 0,
|
|
||||||
"ignore_xss_filter": 0,
|
|
||||||
"in_filter": 0,
|
|
||||||
"in_global_search": 0,
|
|
||||||
"in_list_view": 1,
|
|
||||||
"in_standard_filter": 0,
|
|
||||||
"label": "DN Detail",
|
|
||||||
"length": 0,
|
|
||||||
"no_copy": 0,
|
|
||||||
"permlevel": 0,
|
|
||||||
"print_hide": 0,
|
|
||||||
"print_hide_if_no_value": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"remember_last_selected_value": 0,
|
|
||||||
"report_hide": 0,
|
|
||||||
"reqd": 0,
|
|
||||||
"search_index": 0,
|
|
||||||
"set_only_once": 0,
|
|
||||||
"unique": 0
|
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"hide_heading": 0,
|
"idx": 1,
|
||||||
"hide_toolbar": 0,
|
"istable": 1,
|
||||||
"idx": 1,
|
"links": [],
|
||||||
"image_view": 0,
|
"modified": "2021-12-14 01:22:00.715935",
|
||||||
"in_create": 0,
|
"modified_by": "Administrator",
|
||||||
|
"module": "Stock",
|
||||||
"is_submittable": 0,
|
"name": "Packing Slip Item",
|
||||||
"issingle": 0,
|
"naming_rule": "Random",
|
||||||
"istable": 1,
|
"owner": "Administrator",
|
||||||
"max_attachments": 0,
|
"permissions": [],
|
||||||
"modified": "2018-06-01 07:21:58.220980",
|
"sort_field": "modified",
|
||||||
"modified_by": "Administrator",
|
"sort_order": "DESC",
|
||||||
"module": "Stock",
|
"track_changes": 1
|
||||||
"name": "Packing Slip Item",
|
|
||||||
"owner": "Administrator",
|
|
||||||
"permissions": [],
|
|
||||||
"quick_entry": 0,
|
|
||||||
"read_only": 0,
|
|
||||||
"read_only_onload": 0,
|
|
||||||
"show_name_in_global_search": 0,
|
|
||||||
"track_changes": 1,
|
|
||||||
"track_seen": 0
|
|
||||||
}
|
}
|
||||||
@@ -118,6 +118,10 @@ class PurchaseReceipt(BuyingController):
|
|||||||
if getdate(self.posting_date) > getdate(nowdate()):
|
if getdate(self.posting_date) > getdate(nowdate()):
|
||||||
throw(_("Posting Date cannot be future date"))
|
throw(_("Posting Date cannot be future date"))
|
||||||
|
|
||||||
|
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_cwip_accounts(self):
|
def validate_cwip_accounts(self):
|
||||||
for item in self.get('items'):
|
for item in self.get('items'):
|
||||||
|
|||||||
@@ -168,8 +168,8 @@ def repost_entries():
|
|||||||
for row in riv_entries:
|
for row in riv_entries:
|
||||||
doc = frappe.get_doc('Repost Item Valuation', row.name)
|
doc = frappe.get_doc('Repost Item Valuation', row.name)
|
||||||
if doc.status in ('Queued', 'In Progress'):
|
if doc.status in ('Queued', 'In Progress'):
|
||||||
doc.deduplicate_similar_repost()
|
|
||||||
repost(doc)
|
repost(doc)
|
||||||
|
doc.deduplicate_similar_repost()
|
||||||
|
|
||||||
riv_entries = get_repost_item_valuation_entries()
|
riv_entries = get_repost_item_valuation_entries()
|
||||||
if riv_entries:
|
if riv_entries:
|
||||||
|
|||||||
@@ -4,12 +4,14 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
|
from frappe.utils import nowdate
|
||||||
|
|
||||||
from erpnext.controllers.stock_controller import create_item_wise_repost_entries
|
from erpnext.controllers.stock_controller import create_item_wise_repost_entries
|
||||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||||
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
|
from erpnext.stock.doctype.repost_item_valuation.repost_item_valuation import (
|
||||||
in_configured_timeslot,
|
in_configured_timeslot,
|
||||||
)
|
)
|
||||||
|
from erpnext.stock.utils import PendingRepostingError
|
||||||
|
|
||||||
|
|
||||||
class TestRepostItemValuation(unittest.TestCase):
|
class TestRepostItemValuation(unittest.TestCase):
|
||||||
@@ -138,3 +140,25 @@ class TestRepostItemValuation(unittest.TestCase):
|
|||||||
# to avoid breaking other tests accidentaly
|
# to avoid breaking other tests accidentaly
|
||||||
riv4.set_status("Skipped")
|
riv4.set_status("Skipped")
|
||||||
riv3.set_status("Skipped")
|
riv3.set_status("Skipped")
|
||||||
|
|
||||||
|
def test_stock_freeze_validation(self):
|
||||||
|
|
||||||
|
today = nowdate()
|
||||||
|
|
||||||
|
riv = frappe.get_doc(
|
||||||
|
doctype="Repost Item Valuation",
|
||||||
|
item_code="_Test Item",
|
||||||
|
warehouse="_Test Warehouse - _TC",
|
||||||
|
based_on="Item and Warehouse",
|
||||||
|
posting_date=today,
|
||||||
|
posting_time="00:01:00",
|
||||||
|
)
|
||||||
|
riv.flags.dont_run_in_test = True # keep it queued
|
||||||
|
riv.submit()
|
||||||
|
|
||||||
|
stock_settings = frappe.get_doc("Stock Settings")
|
||||||
|
stock_settings.stock_frozen_upto = today
|
||||||
|
|
||||||
|
self.assertRaises(PendingRepostingError, stock_settings.save)
|
||||||
|
|
||||||
|
riv.set_status("Skipped")
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user