removing currency filter and added rate conversion while fetching timesheets in SI

This commit is contained in:
Anupam 2021-05-16 14:17:56 +05:30
commit 35137ba9e0
73 changed files with 1895 additions and 1224 deletions

View File

@ -29,4 +29,5 @@ ignore =
B950, B950,
W191, W191,
max-line-length = 200 max-line-length = 200
exclude=.github/helper/semgrep_rules

View File

@ -4,25 +4,61 @@ from frappe import _, flt
from frappe.model.document import Document from frappe.model.document import Document
# ruleid: frappe-modifying-but-not-comitting
def on_submit(self): def on_submit(self):
if self.value_of_goods == 0: if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0')) frappe.throw(_('Value of goods cannot be 0'))
# ruleid: frappe-modifying-after-submit
self.status = 'Submitted' self.status = 'Submitted'
# ok: frappe-modifying-but-not-comitting
def on_submit(self): def on_submit(self):
if flt(self.per_billed) < 100: if self.value_of_goods == 0:
self.update_billing_status() frappe.throw(_('Value of goods cannot be 0'))
else: self.status = 'Submitted'
# todook: frappe-modifying-after-submit self.db_set('status', 'Submitted')
self.status = "Completed"
self.db_set("status", "Completed")
class TestDoc(Document): # ok: frappe-modifying-but-not-comitting
pass def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
x = "y"
self.status = x
self.db_set('status', x)
def validate(self):
#ruleid: frappe-modifying-child-tables-while-iterating # ok: frappe-modifying-but-not-comitting
for item in self.child_table: def on_submit(self):
if item.value < 0: x = "y"
self.remove(item) self.status = x
self.save()
# ruleid: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "uptate"
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "update"
self.db_set("status", "update")
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
self.save()
def tainted_method(self):
self.status = "uptate"

View File

@ -1,32 +1,93 @@
# This file specifies rules for correctness according to how frappe doctype data model works. # This file specifies rules for correctness according to how frappe doctype data model works.
rules: rules:
- id: frappe-modifying-after-submit - id: frappe-modifying-but-not-comitting
patterns: patterns:
- pattern: self.$ATTR = ... - pattern: |
- pattern-inside: | def $METHOD(self, ...):
def on_submit(self, ...):
... ...
self.$ATTR = ...
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.save()
- metavariable-regex: - metavariable-regex:
metavariable: '$ATTR' metavariable: '$ATTR'
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me) # this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
regex: '^(?!status_updater)(.*)$' regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: | message: |
Doctype modified after submission. Please check if modification of self.$ATTR is commited to database. DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
languages: [python] languages: [python]
severity: ERROR severity: ERROR
- id: frappe-modifying-after-cancel - id: frappe-modifying-but-not-comitting-other-method
patterns: patterns:
- pattern: self.$ATTR = ... - pattern: |
- pattern-inside: | class $DOCTYPE(...):
def on_cancel(self, ...): def $METHOD(self, ...):
... ...
- metavariable-regex: self.$ANOTHER_METHOD()
metavariable: '$ATTR' ...
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
self.save()
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: | message: |
Doctype modified after cancellation. Please check if modification of self.$ATTR is commited to database. self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
languages: [python] languages: [python]
severity: ERROR severity: ERROR

View File

@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting // ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' + __('You have {0} subscribers' +
'in your mailing list', [subscribers.length]) 'in your mailing list', [subscribers.length])
// ok: frappe-translation-js-splitting
__("Ctrl+Enter to add comment")
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers \
in your mailing list', [subscribers.length])

View File

@ -42,9 +42,10 @@ rules:
- id: frappe-translation-python-splitting - id: frappe-translation-python-splitting
pattern-either: pattern-either:
- pattern: _(...) + ... + _(...) - pattern: _(...) + _(...)
- pattern: _("..." + "...") - pattern: _("..." + "...")
- pattern-regex: '_\([^\)]*\\\s*' - pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )
message: | message: |
Do not split strings inside translate function. Do not concatenate using translate functions. Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations Please refer: https://frappeframework.com/docs/user/en/translations
@ -53,8 +54,8 @@ rules:
- id: frappe-translation-js-splitting - id: frappe-translation-js-splitting
pattern-either: pattern-either:
- pattern-regex: '__\([^\)]*[\+\\]\s*' - pattern-regex: '__\([^\)]*[\\]\s+'
- pattern: __('...' + '...') - pattern: __('...' + '...', ...)
- pattern: __('...') + __('...') - pattern: __('...') + __('...')
message: | message: |
Do not split strings inside translate function. Do not concatenate using translate functions. Do not split strings inside translate function. Do not concatenate using translate functions.

View File

@ -4,6 +4,8 @@ on:
pull_request: pull_request:
branches: branches:
- develop - develop
- version-13-hotfix
- version-13-pre-release
jobs: jobs:
semgrep: semgrep:
name: Frappe Linter name: Frappe Linter
@ -14,11 +16,19 @@ jobs:
uses: actions/setup-python@v2 uses: actions/setup-python@v2
with: with:
python-version: 3.8 python-version: 3.8
- name: Run semgrep
- name: Setup semgrep
run: | run: |
python -m pip install -q semgrep python -m pip install -q semgrep
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
- name: Semgrep errors
run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files [[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
semgrep --config="r/python.lang.correctness" --quiet --error $files semgrep --config="r/python.lang.correctness" --quiet --error $files
- name: Semgrep warnings
run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files [[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files

View File

@ -239,6 +239,7 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal", "withdrawal",
"description", "description",
"reference_number", "reference_number",
"bank_account"
], ],
}, },
}); });

View File

@ -146,7 +146,7 @@
}, },
{ {
"depends_on": "eval:!doc.__islocal && !doc.import_file\n", "depends_on": "eval:!doc.__islocal && !doc.import_file\n",
"description": "Must be a publicly accessible Google Sheets URL", "description": "Must be a publicly accessible Google Sheets URL and adding Bank Account column is necessary for importing via Google Sheets",
"fieldname": "google_sheets_url", "fieldname": "google_sheets_url",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Import from Google Sheets" "label": "Import from Google Sheets"
@ -202,7 +202,7 @@
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"links": [], "links": [],
"modified": "2021-02-10 19:29:59.027325", "modified": "2021-05-12 14:17:37.777246",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Statement Import", "name": "Bank Statement Import",
@ -224,4 +224,4 @@
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "track_changes": 1
} }

View File

@ -47,6 +47,13 @@ class BankStatementImport(DataImport):
def start_import(self): def start_import(self):
preview = frappe.get_doc("Bank Statement Import", self.name).get_preview_from_template(
self.import_file, self.google_sheets_url
)
if 'Bank Account' not in json.dumps(preview):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
@ -67,6 +74,7 @@ class BankStatementImport(DataImport):
data_import=self.name, data_import=self.name,
bank_account=self.bank_account, bank_account=self.bank_account,
import_file_path=self.import_file, import_file_path=self.import_file,
google_sheets_url=self.google_sheets_url,
bank=self.bank, bank=self.bank,
template_options=self.template_options, template_options=self.template_options,
now=frappe.conf.developer_mode or frappe.flags.in_test, now=frappe.conf.developer_mode or frappe.flags.in_test,
@ -90,18 +98,20 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name) data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows() data_import.export_errored_rows()
def start_import(data_import, bank_account, import_file_path, bank, template_options): def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
"""This method runs in background job""" """This method runs in background job"""
update_mapping_db(bank, template_options) update_mapping_db(bank, template_options)
data_import = frappe.get_doc("Bank Statement Import", data_import) data_import = frappe.get_doc("Bank Statement Import", data_import)
file = import_file_path if import_file_path else google_sheets_url
import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records") import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
data = import_file.raw_data data = import_file.raw_data
add_bank_account(data, bank_account) if import_file_path:
write_files(import_file, data) add_bank_account(data, bank_account)
write_files(import_file, data)
try: try:
i = Importer(data_import.reference_doctype, data_import=data_import) i = Importer(data_import.reference_doctype, data_import=data_import)

View File

@ -1,196 +1,82 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "creation": "2018-01-02 15:48:58.768352",
"allow_import": 0, "doctype": "DocType",
"allow_rename": 0, "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2018-01-02 15:48:58.768352", "field_order": [
"custom": 0, "company",
"docstatus": 0, "cgst_account",
"doctype": "DocType", "sgst_account",
"document_type": "", "igst_account",
"editable_grid": 1, "cess_account",
"engine": "InnoDB", "is_reverse_charge_account"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "columns": 1,
"allow_on_submit": 0, "fieldname": "company",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "Company",
"fieldname": "company", "options": "Company",
"fieldtype": "Link", "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": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0, "fieldname": "cgst_account",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "CGST Account",
"fieldname": "cgst_account", "options": "Account",
"fieldtype": "Link", "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": "CGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0, "fieldname": "sgst_account",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "SGST Account",
"fieldname": "sgst_account", "options": "Account",
"fieldtype": "Link", "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": "SGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0, "fieldname": "igst_account",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "IGST Account",
"fieldname": "igst_account", "options": "Account",
"fieldtype": "Link", "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": "IGST Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{ {
"allow_bulk_edit": 0, "columns": 2,
"allow_on_submit": 0, "fieldname": "cess_account",
"bold": 0, "fieldtype": "Link",
"collapsible": 0, "in_list_view": 1,
"columns": 0, "label": "CESS Account",
"fieldname": "cess_account", "options": "Account"
"fieldtype": "Link", },
"hidden": 0, {
"ignore_user_permissions": 0, "columns": 1,
"ignore_xss_filter": 0, "default": "0",
"in_filter": 0, "fieldname": "is_reverse_charge_account",
"in_global_search": 0, "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Is Reverse Charge Account"
"label": "CESS Account",
"length": 0,
"no_copy": 0,
"options": "Account",
"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
} }
], ],
"has_web_view": 0, "index_web_pages_for_search": 1,
"hide_heading": 0, "istable": 1,
"hide_toolbar": 0, "links": [],
"idx": 0, "modified": "2021-04-09 12:30:25.889993",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Accounts",
"is_submittable": 0, "name": "GST Account",
"issingle": 0, "owner": "Administrator",
"istable": 1, "permissions": [],
"max_attachments": 0, "quick_entry": 1,
"modified": "2018-01-02 15:52:22.335988", "sort_field": "modified",
"modified_by": "Administrator", "sort_order": "DESC",
"module": "Accounts", "track_changes": 1
"name": "GST Account",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
} }

View File

@ -1380,7 +1380,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-03-30 22:45:58.334107", "modified": "2021-04-30 22:45:58.334107",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this; var me = this;
this._super(); this._super();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice']; this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format // show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0); this.frm.set_df_property("debit_to", "print_hide", 0);
@ -582,6 +582,16 @@ frappe.ui.form.on('Sales Invoice', {
}; };
}); });
frm.set_query("adjustment_against", function() {
return {
filters: {
company: frm.doc.company,
customer: frm.doc.customer,
docstatus: 1
}
};
});
frm.custom_make_buttons = { frm.custom_make_buttons = {
'Delivery Note': 'Delivery', 'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note', 'Sales Invoice': 'Return / Credit Note',
@ -809,8 +819,20 @@ frappe.ui.form.on('Sales Invoice', {
} }
}, },
add_timesheet_row: function(frm, row, exchange_rate) {
frm.add_child('timesheets',{
'activity_type': row.activity_type,
'description': row.description,
'time_sheet': row.parent,
'billing_hours': row.billing_hours,
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
'timesheet_detail': row.name
});
frm.refresh_field('timesheets')
},
refresh: function(frm) { refresh: function(frm) {
if (frm.doc.project && frm.doc.docstatus===0 && !frm.doc.is_return) { if (frm.doc.docstatus===0 && !frm.doc.is_return) {
frm.add_custom_button(__('Fetch Timesheet'), function() { frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({ let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'), title: __('Fetch Timesheet'),
@ -821,15 +843,6 @@ frappe.ui.form.on('Sales Invoice', {
"fieldtype": "Date", "fieldtype": "Date",
"reqd": 1, "reqd": 1,
}, },
{
"label" : __("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"default": frm.doc.currency,
"reqd": 1,
"read_only": 1
},
{ {
fieldtype: 'Column Break', fieldtype: 'Column Break',
fieldname: 'col_break_1', fieldname: 'col_break_1',
@ -845,9 +858,7 @@ frappe.ui.form.on('Sales Invoice', {
"fieldname": "project", "fieldname": "project",
"fieldtype": "Link", "fieldtype": "Link",
"options": "Project", "options": "Project",
"default": frm.doc.project, "default": frm.doc.project
"reqd": 1,
"read_only": 1
}, },
], ],
primary_action: function() { primary_action: function() {
@ -857,24 +868,33 @@ frappe.ui.form.on('Sales Invoice', {
args: { args: {
from_time: data.from_time, from_time: data.from_time,
to_time: data.to_time, to_time: data.to_time,
project: data.project, project: data.project
currency: data.currency
}, },
callback: function(r) { callback: function(r) {
if(!r.exc) { if(!r.exc) {
if(r.message.length > 0) { if(r.message.length > 0) {
frm.clear_table('timesheets') frm.clear_table('timesheets')
r.message.forEach((d) => { r.message.forEach((d) => {
frm.add_child('timesheets',{ let exchange_rate = 1.0;
'activity_type': d.activity_type, if (frm.doc.currency != d.currency) {
'description': d.description, frappe.call({
'time_sheet': d.parent, method: "erpnext.setup.utils.get_exchange_rate",
'billing_hours': d.billing_hours, args: {
'billing_amount': d.billing_amount, from_currency: d.currency,
'timesheet_detail': d.name to_currency: frm.doc.currency
}); },
callback: function(r) {
if (r.message) {
exchange_rate = r.message;
frm.events.add_timesheet_row(frm, d, exchange_rate);
}
}
});
}
else {
frm.events.add_timesheet_row(frm, d, exchange_rate);
}
}); });
frm.refresh_field('timesheets')
} }
else { else {
frappe.msgprint(__('No Timesheet Found.')) frappe.msgprint(__('No Timesheet Found.'))
@ -890,6 +910,10 @@ frappe.ui.form.on('Sales Invoice', {
}) })
} }
if (frm.doc.is_debit_note) {
frm.set_df_property('return_against', 'label', 'Adjustment Against');
}
if (frappe.boot.active_domains.includes("Healthcare")) { if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0); frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0); frm.set_df_property("patient_name", "hidden", 0);

View File

@ -16,6 +16,7 @@
"is_pos", "is_pos",
"is_consolidated", "is_consolidated",
"is_return", "is_return",
"is_debit_note",
"update_billed_amount_in_sales_order", "update_billed_amount_in_sales_order",
"column_break1", "column_break1",
"company", "company",
@ -392,7 +393,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "return_against", "depends_on": "eval:doc.return_against || doc.is_debit_note",
"fieldname": "return_against", "fieldname": "return_against",
"fieldtype": "Link", "fieldtype": "Link",
"hide_days": 1, "hide_days": 1,
@ -401,7 +402,7 @@
"no_copy": 1, "no_copy": 1,
"options": "Sales Invoice", "options": "Sales Invoice",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only_depends_on": "eval:doc.is_return",
"search_index": 1 "search_index": 1
}, },
{ {
@ -1954,6 +1955,12 @@
}, },
{ {
"default": "0", "default": "0",
"fieldname": "is_debit_note",
"fieldtype": "Check",
"label": "Is Debit Note"
},
{
"default": 0,
"depends_on": "grand_total", "depends_on": "grand_total",
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",

View File

@ -21,7 +21,10 @@ def get_party_details(inv):
else: else:
party_type = 'Supplier' party_type = 'Supplier'
party = inv.supplier party = inv.supplier
if not party:
frappe.throw(_("Please select {0} first").format(party_type))
return party_type, party return party_type, party
def get_party_tax_withholding_details(inv, tax_withholding_category=None): def get_party_tax_withholding_details(inv, tax_withholding_category=None):
@ -324,7 +327,7 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
net_total, ldc.certificate_limit net_total, ldc.certificate_limit
): ):
tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
return tds_amount return tds_amount
def get_debit_note_amount(suppliers, fiscal_year_details, company=None): def get_debit_note_amount(suppliers, fiscal_year_details, company=None):

View File

@ -135,7 +135,7 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit
# from consolidated financial statement # from consolidated financial statement
if filters.get('accumulated_in_group_company'): if filters.get('accumulated_in_group_company'):
period_list = get_filtered_list_for_consolidated_report(period_list) period_list = get_filtered_list_for_consolidated_report(filters, period_list)
for period in period_list: for period in period_list:
key = period if consolidated else period.key key = period if consolidated else period.key

View File

@ -15,6 +15,7 @@
"hide_custom": 0, "hide_custom": 0,
"icon": "accounting", "icon": "accounting",
"idx": 0, "idx": 0,
"is_default": 0,
"is_standard": 1, "is_standard": 1,
"label": "Accounting", "label": "Accounting",
"links": [ "links": [
@ -625,9 +626,9 @@
"dependencies": "", "dependencies": "",
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
"label": "Bank Reconciliation", "label": "Bank Reconciliation Tool",
"link_to": "bank-reconciliation", "link_to": "Bank Reconciliation Tool",
"link_type": "Page", "link_type": "DocType",
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
@ -641,26 +642,6 @@
"onboard": 0, "onboard": 0,
"type": "Link" "type": "Link"
}, },
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Statement Transaction Entry",
"link_to": "Bank Statement Transaction Entry",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Bank Statement Settings",
"link_to": "Bank Statement Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -1071,7 +1052,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2021-03-04 00:38:35.349024", "modified": "2021-05-12 11:48:01.905144",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting", "name": "Accounting",

View File

@ -195,8 +195,7 @@ class Asset(AccountsController):
# 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 = self.get_depreciation_amount(value_after_depreciation, depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
d.total_number_of_depreciations, d)
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(d.depreciation_start_date,
@ -208,7 +207,7 @@ class Asset(AccountsController):
# For first row # For first row
if has_pro_rata and n==0: if has_pro_rata and n==0:
depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount, depreciation_amount, days, months = self.get_pro_rata_amt(d, depreciation_amount,
self.available_for_use_date, d.depreciation_start_date) self.available_for_use_date, d.depreciation_start_date)
# For first depr schedule date will be the start date # For first depr schedule date will be the start date
@ -220,7 +219,7 @@ class Asset(AccountsController):
to_date = add_months(self.available_for_use_date, to_date = add_months(self.available_for_use_date,
n * cint(d.frequency_of_depreciation)) n * cint(d.frequency_of_depreciation))
depreciation_amount, days, months = get_pro_rata_amt(d, depreciation_amount, days, months = self.get_pro_rata_amt(d,
depreciation_amount, schedule_date, to_date) depreciation_amount, schedule_date, to_date)
monthly_schedule_date = add_months(schedule_date, 1) monthly_schedule_date = add_months(schedule_date, 1)
@ -365,24 +364,6 @@ class Asset(AccountsController):
def get_value_after_depreciation(self, idx): def get_value_after_depreciation(self, idx):
return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation) return flt(self.get('finance_books')[cint(idx)-1].value_after_depreciation)
def get_depreciation_amount(self, depreciable_value, total_number_of_depreciations, row):
precision = self.precision("gross_purchase_amount")
if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_left = (cint(row.total_number_of_depreciations) - cint(self.number_of_depreciations_booked))
if not depreciation_left:
frappe.msgprint(_("All the depreciations has been booked"))
depreciation_amount = flt(row.expected_value_after_useful_life)
return depreciation_amount
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100), precision)
return depreciation_amount
def validate_expected_value_after_useful_life(self): def validate_expected_value_after_useful_life(self):
for row in self.get('finance_books'): for row in self.get('finance_books'):
accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount
@ -575,6 +556,13 @@ class Asset(AccountsController):
return 100 * (1 - flt(depreciation_rate, float_precision)) return 100 * (1 - flt(depreciation_rate, float_precision))
def get_pro_rata_amt(self, row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def update_maintenance_status(): def update_maintenance_status():
assets = frappe.get_all( assets = frappe.get_all(
"Asset", filters={"docstatus": 1, "maintenance_required": 1} "Asset", filters={"docstatus": 1, "maintenance_required": 1}
@ -758,15 +746,20 @@ def make_asset_movement(assets, purpose=None):
def is_cwip_accounting_enabled(asset_category): def is_cwip_accounting_enabled(asset_category):
return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting"))
def get_pro_rata_amt(row, depreciation_amount, from_date, to_date):
days = date_diff(to_date, from_date)
months = month_diff(to_date, from_date)
total_days = get_total_days(to_date, row.frequency_of_depreciation)
return (depreciation_amount * flt(days)) / flt(total_days), days, months
def get_total_days(date, frequency): def get_total_days(date, frequency):
period_start_date = add_months(date, period_start_date = add_months(date,
cint(frequency) * -1) cint(frequency) * -1)
return date_diff(date, period_start_date) return date_diff(date, period_start_date)
@erpnext.allow_regional
def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
else:
depreciation_amount = flt(depreciable_value * (flt(row.rate_of_depreciation) / 100))
return depreciation_amount

View File

@ -635,6 +635,45 @@ class TestAsset(unittest.TestCase):
frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc)
frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc)
def test_discounted_wdv_depreciation_rate_for_indian_region(self):
# set indian company
company_flag = frappe.flags.company
frappe.flags.company = "_Test Company"
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location")
asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name')
asset = frappe.get_doc('Asset', asset_name)
asset.calculate_depreciation = 1
asset.available_for_use_date = '2030-06-12'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value",
"total_number_of_depreciations": 3,
"frequency_of_depreciation": 12,
"depreciation_start_date": "2030-12-31"
})
asset.save(ignore_permissions=True)
self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0)
expected_schedules = [
["2030-12-31", 1106.85, 1106.85],
["2031-12-31", 3446.58, 4553.43],
["2032-12-31", 1723.29, 6276.72],
["2033-06-12", 723.28, 7000.00]
]
schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)]
for d in asset.get("schedules")]
self.assertEqual(schedules, expected_schedules)
# reset indian company
frappe.flags.company = company_flag
def create_asset_data(): def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"): if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category() create_asset_category()

View File

@ -62,6 +62,7 @@ class RequestforQuotation(BuyingController):
for supplier in self.suppliers: for supplier in self.suppliers:
supplier.email_sent = 0 supplier.email_sent = 0
supplier.quote_status = 'Pending' supplier.quote_status = 'Pending'
self.send_to_supplier()
def on_cancel(self): def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled') frappe.db.set(self, 'status', 'Cancelled')
@ -81,7 +82,7 @@ class RequestforQuotation(BuyingController):
def send_to_supplier(self): def send_to_supplier(self):
"""Sends RFQ mail to involved suppliers.""" """Sends RFQ mail to involved suppliers."""
for rfq_supplier in self.suppliers: for rfq_supplier in self.suppliers:
if rfq_supplier.send_email: if rfq_supplier.email_id is not None and rfq_supplier.send_email:
self.validate_email_id(rfq_supplier) self.validate_email_id(rfq_supplier)
# make new user if required # make new user if required

View File

@ -368,6 +368,11 @@ class AccountsController(TransactionBase):
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
# Double check for cost center
# Items add via promotional scheme may not have cost center set
if hasattr(item, 'cost_center') and not item.get('cost_center'):
item.set('cost_center', self.get('cost_center') or erpnext.get_default_cost_center(self.company))
if ret.get("pricing_rules"): if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret) self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret) self.set_pricing_rule_details(item, ret)

View File

@ -100,6 +100,10 @@ status_map = {
["Queued", "eval:self.status == 'Queued'"], ["Queued", "eval:self.status == 'Queued'"],
["Failed", "eval:self.status == 'Failed'"], ["Failed", "eval:self.status == 'Failed'"],
["Cancelled", "eval:self.docstatus == 2"], ["Cancelled", "eval:self.docstatus == 2"],
],
"Transaction Deletion Record": [
["Draft", None],
["Completed", "eval:self.docstatus == 1"],
] ]
} }

View File

@ -379,8 +379,7 @@ class StockController(AccountsController):
link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection) link = frappe.utils.get_link_to_form('Quality Inspection', d.quality_inspection)
frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError) frappe.throw(_("Quality Inspection: {0} is not submitted for the item: {1} in row {2}").format(link, d.item_code, d.idx), QualityInspectionNotSubmittedError)
qa_failed = any([r.status=="Rejected" for r in qa_doc.readings]) if qa_doc.status != 'Accepted':
if qa_failed:
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}") frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
.format(d.idx, d.item_code), QualityInspectionRejectedError) .format(d.idx, d.item_code), QualityInspectionRejectedError)
elif qa_required : elif qa_required :

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, base64, hashlib, hmac, json import frappe, base64, hashlib, hmac, json
from frappe.utils import cstr
from frappe import _ from frappe import _
def verify_request(): def verify_request():
@ -146,22 +147,19 @@ def rename_address(address, customer):
def link_items(items_list, woocommerce_settings, sys_lang): def link_items(items_list, woocommerce_settings, sys_lang):
for item_data in items_list: for item_data in items_list:
item_woo_com_id = item_data.get("product_id") item_woo_com_id = cstr(item_data.get("product_id"))
if frappe.get_value("Item", {"woocommerce_id": item_woo_com_id}): if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, 'name'):
#Edit Item
item = frappe.get_doc("Item", {"woocommerce_id": item_woo_com_id})
else:
#Create Item #Create Item
item = frappe.new_doc("Item") item = frappe.new_doc("Item")
item.item_code = _("woocommerce - {0}", sys_lang).format(item_woo_com_id)
item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
item.item_group = _("WooCommerce Products", sys_lang)
item.item_name = item_data.get("name") item.item_name = item_data.get("name")
item.item_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id")) item.woocommerce_id = item_woo_com_id
item.woocommerce_id = item_data.get("product_id") item.flags.ignore_mandatory = True
item.item_group = _("WooCommerce Products", sys_lang) item.save()
item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
item.flags.ignore_mandatory = True
item.save()
def create_sales_order(order, woocommerce_settings, customer_name, sys_lang): def create_sales_order(order, woocommerce_settings, customer_name, sys_lang):
new_sales_order = frappe.new_doc("Sales Order") new_sales_order = frappe.new_doc("Sales Order")
@ -194,12 +192,12 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l
for item in order.get("line_items"): for item in order.get("line_items"):
woocomm_item_id = item.get("product_id") woocomm_item_id = item.get("product_id")
found_item = frappe.get_doc("Item", {"woocommerce_id": woocomm_item_id}) found_item = frappe.get_doc("Item", {"woocommerce_id": cstr(woocomm_item_id)})
ordered_items_tax = item.get("total_tax") ordered_items_tax = item.get("total_tax")
new_sales_order.append("items",{ new_sales_order.append("items", {
"item_code": found_item.item_code, "item_code": found_item.name,
"item_name": found_item.item_name, "item_name": found_item.item_name,
"description": found_item.item_name, "description": found_item.item_name,
"delivery_date": new_sales_order.delivery_date, "delivery_date": new_sales_order.delivery_date,
@ -207,7 +205,7 @@ def set_items_in_sales_order(new_sales_order, woocommerce_settings, order, sys_l
"qty": item.get("quantity"), "qty": item.get("quantity"),
"rate": item.get("price"), "rate": item.get("price"),
"warehouse": woocommerce_settings.warehouse or default_warehouse "warehouse": woocommerce_settings.warehouse or default_warehouse
}) })
add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account) add_tax_details(new_sales_order, ordered_items_tax, "Ordered Item tax", woocommerce_settings.tax_account)

View File

@ -90,9 +90,9 @@ def add_bank_accounts(response, bank, company):
"bank": bank["bank_name"], "bank": bank["bank_name"],
"account": default_gl_account.account, "account": default_gl_account.account,
"account_name": account["name"], "account_name": account["name"],
"account_type": account["type"] or "", "account_type": account.get("type", ""),
"account_subtype": account["subtype"] or "", "account_subtype": account.get("subtype", ""),
"mask": account["mask"] or "", "mask": account.get("mask", ""),
"integration_id": account["id"], "integration_id": account["id"],
"is_company_account": 1, "is_company_account": 1,
"company": company "company": company

View File

@ -32,10 +32,12 @@ def create_customer(shopify_customer, shopify_settings):
raise e raise e
def create_customer_address(customer, shopify_customer): def create_customer_address(customer, shopify_customer):
if not shopify_customer.get("addresses"): addresses = shopify_customer.get("addresses", [])
return
for i, address in enumerate(shopify_customer.get("addresses")): if not addresses and "default_address" in shopify_customer:
addresses.append(shopify_customer["default_address"])
for i, address in enumerate(addresses):
address_title, address_type = get_address_title_and_type(customer.customer_name, i) address_title, address_type = get_address_title_and_type(customer.customer_name, i)
try : try :
frappe.get_doc({ frappe.get_doc({

View File

@ -268,10 +268,12 @@ doc_events = {
}, },
"Purchase Invoice": { "Purchase Invoice": {
"validate": [ "validate": [
"erpnext.regional.india.utils.update_grand_total_for_rcm", "erpnext.regional.india.utils.validate_reverse_charge_transaction",
"erpnext.regional.india.utils.update_itc_availed_fields",
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm", "erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
"erpnext.regional.united_arab_emirates.utils.validate_returns" "erpnext.regional.united_arab_emirates.utils.validate_returns",
] "erpnext.regional.india.utils.update_taxable_values"
]
}, },
"Payment Entry": { "Payment Entry": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"], "on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
@ -304,8 +306,7 @@ doc_events = {
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry. # if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
auto_cancel_exempted_doctypes= [ auto_cancel_exempted_doctypes= [
"Payment Entry", "Payment Entry",
"Inpatient Medication Entry", "Inpatient Medication Entry"
"Timesheet"
] ]
after_migrate = ["erpnext.setup.install.update_select_perm_after_install"] after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
@ -366,10 +367,8 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send", "erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms", "erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation", "erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
"erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment", "erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves", "erpnext.hr.utils.allocate_earned_leaves",
"erpnext.hr.utils.grant_leaves_automatically",
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall", "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans", "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead" "erpnext.crm.doctype.lead.lead.daily_open_lead"
@ -426,8 +425,8 @@ regional_overrides = {
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts', 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption', 'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period', 'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries', 'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields' 'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount'
}, },
'United Arab Emirates': { 'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data', 'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',

View File

@ -11,8 +11,12 @@ def get_data():
}, },
'transactions': [ 'transactions': [
{ {
'label': _('Leave and Attendance'), 'label': _('Attendance'),
'items': ['Attendance', 'Attendance Request', 'Leave Application', 'Leave Allocation', 'Employee Checkin'] 'items': ['Attendance', 'Attendance Request', 'Employee Checkin']
},
{
'label': _('Leave'),
'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment']
}, },
{ {
'label': _('Lifecycle'), 'label': _('Lifecycle'),
@ -30,10 +34,6 @@ def get_data():
'label': _('Benefit'), 'label': _('Benefit'),
'items': ['Employee Benefit Application', 'Employee Benefit Claim'] 'items': ['Employee Benefit Application', 'Employee Benefit Claim']
}, },
{
'label': _('Evaluation'),
'items': ['Appraisal']
},
{ {
'label': _('Payroll'), 'label': _('Payroll'),
'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account'] 'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account']
@ -42,5 +42,9 @@ def get_data():
'label': _('Training'), 'label': _('Training'),
'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map'] 'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map']
}, },
{
'label': _('Evaluation'),
'items': ['Appraisal']
},
] ]
} }

View File

@ -23,7 +23,6 @@
"show_leaves_of_all_department_members_in_calendar", "show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment", "auto_leave_encashment",
"restrict_backdated_leave_application", "restrict_backdated_leave_application",
"automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings", "hiring_settings",
"check_vacancies" "check_vacancies"
], ],
@ -133,12 +132,6 @@
"label": "Role Allowed to Create Backdated Leave Application", "label": "Role Allowed to Create Backdated Leave Application",
"options": "Role" "options": "Role"
}, },
{
"default": "0",
"fieldname": "automatically_allocate_leaves_based_on_leave_policy",
"fieldtype": "Check",
"label": "Automatically Allocate Leaves Based On Leave Policy"
},
{ {
"default": "1", "default": "1",
"fieldname": "send_leave_notification", "fieldname": "send_leave_notification",
@ -155,7 +148,7 @@
"idx": 1, "idx": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-04-26 10:52:56.192773", "modified": "2021-05-11 10:52:56.192773",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "HR Settings", "name": "HR Settings",

View File

@ -446,8 +446,6 @@ class TestLeaveApplication(unittest.TestCase):
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
from erpnext.hr.utils import allocate_earned_leaves from erpnext.hr.utils import allocate_earned_leaves
i = 0 i = 0
while(i<14): while(i<14):

View File

@ -44,10 +44,6 @@ class TestLeaveEncashment(unittest.TestCase):
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee, salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50}) other_details={"leave_encashment_amount_per_day": 50})
#grant Leaves
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
def tearDown(self): def tearDown(self):
for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]: for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]:
frappe.db.sql("delete from `tab%s`" % dt) frappe.db.sql("delete from `tab%s`" % dt)

View File

@ -7,7 +7,7 @@ def get_data():
'transactions': [ 'transactions': [
{ {
'label': _('Leaves'), 'label': _('Leaves'),
'items': ['Leave Allocation'] 'items': ['Leave Policy Assignment', 'Leave Allocation']
}, },
] ]
} }

View File

@ -4,35 +4,22 @@
frappe.ui.form.on('Leave Policy Assignment', { frappe.ui.form.on('Leave Policy Assignment', {
onload: function(frm) { onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"]; frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
},
refresh: function(frm) { frm.set_query('leave_policy', function() {
if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) { return {
frm.add_custom_button(__("Grant Leave"), function() { filters: {
"docstatus": 1
frappe.call({ }
doc: frm.doc, };
method: "grant_leave_alloc_for_employee", });
callback: function(r) { frm.set_query('leave_period', function() {
let leave_allocations = r.message; return {
let msg = frm.events.get_success_message(leave_allocations); filters: {
frappe.msgprint(msg); "is_active": 1,
cur_frm.refresh(); "company": frm.doc.company
} }
}); };
}); });
}
},
get_success_message: function(leave_allocations) {
let msg = __("Leaves has been granted successfully");
msg += "<br><table class='table table-bordered'>";
msg += "<tr><th>"+__('Leave Type')+"</th><th>"+__("Leave Allocation")+"</th><th>"+__("Leaves Granted")+"</th><tr>";
for (let key in leave_allocations) {
msg += "<tr><th>"+key+"</th><td>"+leave_allocations[key]["name"]+"</td><td>"+leave_allocations[key]["leaves"]+"</td></tr>";
}
msg += "</table>";
return msg;
}, },
assignment_based_on: function(frm) { assignment_based_on: function(frm) {

View File

@ -17,6 +17,9 @@ class LeavePolicyAssignment(Document):
self.validate_policy_assignment_overlap() self.validate_policy_assignment_overlap()
self.set_dates() self.set_dates()
def on_submit(self):
self.grant_leave_alloc_for_employee()
def set_dates(self): def set_dates(self):
if self.assignment_based_on == "Leave Period": if self.assignment_based_on == "Leave Period":
self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"]) self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
@ -75,7 +78,7 @@ class LeavePolicyAssignment(Document):
from_date=self.effective_from, from_date=self.effective_from,
to_date=self.effective_to, to_date=self.effective_to,
new_leaves_allocated=new_leaves_allocated, new_leaves_allocated=new_leaves_allocated,
leave_period=self.leave_period or None, leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else '',
leave_policy_assignment = self.name, leave_policy_assignment = self.name,
leave_policy = self.leave_policy, leave_policy = self.leave_policy,
carry_forward=carry_forward carry_forward=carry_forward
@ -131,22 +134,6 @@ class LeavePolicyAssignment(Document):
return new_leaves_allocated return new_leaves_allocated
@frappe.whitelist()
def grant_leave_for_multiple_employees(leave_policy_assignments):
leave_policy_assignments = json.loads(leave_policy_assignments)
not_granted = []
for assignment in leave_policy_assignments:
try:
frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee()
except Exception:
not_granted.append(assignment)
if len(not_granted):
msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents")
else:
msg = _("Leave granted Successfully")
frappe.msgprint(msg)
@frappe.whitelist() @frappe.whitelist()
def create_assignment_for_multiple_employees(employees, data): def create_assignment_for_multiple_employees(employees, data):
@ -166,29 +153,18 @@ def create_assignment_for_multiple_employees(employees, data):
assignment.effective_to = getdate(data.effective_to) or None assignment.effective_to = getdate(data.effective_to) or None
assignment.leave_period = data.leave_period or None assignment.leave_period = data.leave_period or None
assignment.carry_forward = data.carry_forward assignment.carry_forward = data.carry_forward
assignment.save() assignment.save()
assignment.submit() try:
assignment.submit()
except frappe.exceptions.ValidationError:
continue
frappe.db.commit()
docs_name.append(assignment.name) docs_name.append(assignment.name)
return docs_name return docs_name
def automatically_allocate_leaves_based_on_leave_policy():
today = getdate()
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value(
'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy'
)
pending_assignments = frappe.get_list(
"Leave Policy Assignment",
filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today}
)
if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy:
for assignment in pending_assignments:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
def get_leave_type_details(): def get_leave_type_details():
leave_type_details = frappe._dict() leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type", leave_types = frappe.get_all("Leave Type",
@ -197,4 +173,3 @@ def get_leave_type_details():
for d in leave_types: for d in leave_types:
leave_type_details.setdefault(d.name, d) leave_type_details.setdefault(d.name, d)
return leave_type_details return leave_type_details

View File

@ -0,0 +1,13 @@
from __future__ import unicode_literals
from frappe import _
def get_data():
return {
'fieldname': 'leave_policy_assignment',
'transactions': [
{
'label': _('Leaves'),
'items': ['Leave Allocation']
},
]
}

View File

@ -6,6 +6,7 @@ frappe.listview_settings['Leave Policy Assignment'] = {
doctype: "Employee", doctype: "Employee",
target: cur_list, target: cur_list,
setters: { setters: {
employee_name: '',
company: '', company: '',
department: '', department: '',
}, },
@ -92,37 +93,6 @@ frappe.listview_settings['Leave Policy Assignment'] = {
} }
}); });
}); });
list_view.page.add_inner_button(__("Grant Leaves"), function () {
me.dialog = new frappe.ui.form.MultiSelectDialog({
doctype: "Leave Policy Assignment",
target: cur_list,
setters: {
company: '',
employee: '',
},
get_query() {
return {
filters: {
docstatus: ['=', 1],
leaves_allocated: ['=', 0]
}
};
},
add_filters_group: 1,
primary_action_label: "Grant Leaves",
action(leave_policy_assignments) {
frappe.call({
method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees',
async: false,
args: {
leave_policy_assignments: leave_policy_assignments
}
});
me.dialog.hide();
}
});
});
}, },
set_effective_date: function () { set_effective_date: function () {

View File

@ -35,7 +35,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload() leave_policy_assignment_doc.reload()
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1) self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
@ -73,7 +72,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data)) leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]) leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload() leave_policy_assignment_doc.reload()

View File

@ -500,13 +500,6 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
total_claimed_amount = sum_of_claimed_amount[0].total_amount total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount return total_claimed_amount
def grant_leaves_automatically():
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
if automatically_allocate_leaves_based_on_leave_policy:
lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
for assignment in lpa:
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
def share_doc_with_approver(doc, user): def share_doc_with_approver(doc, user):
# if approver does not have permissions, share # if approver does not have permissions, share
if not frappe.has_permission(doc=doc, ptype="submit", user=user): if not frappe.has_permission(doc=doc, ptype="submit", user=user):

View File

@ -769,6 +769,7 @@ erpnext.patches.v13_0.rename_discharge_date_in_ip_record
erpnext.patches.v12_0.create_taxable_value_field erpnext.patches.v12_0.create_taxable_value_field
erpnext.patches.v12_0.add_gst_category_in_delivery_note erpnext.patches.v12_0.add_gst_category_in_delivery_note
erpnext.patches.v12_0.purchase_receipt_status erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v12_0.create_itc_reversal_custom_fields
erpnext.patches.v13_0.fix_non_unique_represents_company erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021 erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021

View File

@ -0,0 +1,115 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.regional.india.utils import get_gst_accounts
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
if not company:
return
frappe.reload_doc("regional", "doctype", "gst_settings")
frappe.reload_doc("accounts", "doctype", "gst_account")
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
custom_fields = {
'Journal Entry': [
dict(fieldname='reversal_type', label='Reversal Type',
fieldtype='Select', insert_after='voucher_type', print_hide=1,
options="As per rules 42 & 43 of CGST Rules\nOthers",
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_address', label='Company Address',
fieldtype='Link', options='Address', insert_after='reversal_type',
print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_gstin', label='Company GSTIN',
fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
fetch_from='company_address.gstin',
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
],
'Purchase Invoice': [
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
default="All Other ITC")
],
'Purchase Invoice Item': [
dict(fieldname='taxable_value', label='Taxable Value',
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
print_hide=1)
]
}
create_custom_fields(custom_fields, update=True)
# Patch ITC Availed fields from Data to Currency
# Patch Availed ITC for current fiscal_year
gst_accounts = get_gst_accounts(only_non_reverse_charge=1)
frappe.db.sql("""
UPDATE `tabCustom Field` SET fieldtype='Currency', options='Company:company:default_currency'
WHERE dt = 'Purchase Invoice' and fieldname in ('itc_integrated_tax', 'itc_state_tax', 'itc_central_tax',
'itc_cess_amount')
""")
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0'
WHERE trim(coalesce(itc_integrated_tax, '')) = '' """)
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_state_tax = '0'
WHERE trim(coalesce(itc_state_tax, '')) = '' """)
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_central_tax = '0'
WHERE trim(coalesce(itc_central_tax, '')) = '' """)
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_cess_amount = '0'
WHERE trim(coalesce(itc_cess_amount, '')) = '' """)
# Get purchase invoices
invoices = frappe.get_all('Purchase Invoice',
{'posting_date': ('>=', '2021-04-01'), 'eligibility_for_itc': ('!=', 'Ineligible')},
['name'])
amount_map = {}
if invoices:
invoice_list = set([d.name for d in invoices])
# Get GST applied
amounts = frappe.db.sql("""
SELECT parent, account_head, sum(base_tax_amount_after_discount_amount) as amount
FROM `tabPurchase Taxes and Charges`
where parent in %s
GROUP BY parent, account_head
""", (invoice_list), as_dict=1)
for d in amounts:
amount_map.setdefault(d.parent,
{
'itc_integrated_tax': 0,
'itc_state_tax': 0,
'itc_central_tax': 0,
'itc_cess_amount': 0
})
if d.account_head in gst_accounts.get('igst_account'):
amount_map[d.parent]['itc_integrated_tax'] += d.amount
if d.account_head in gst_accounts.get('cgst_account'):
amount_map[d.parent]['itc_central_tax'] += d.amount
if d.account_head in gst_accounts.get('sgst_account'):
amount_map[d.parent]['itc_state_tax'] += d.amount
if d.account_head in gst_accounts.get('cess_account'):
amount_map[d.parent]['itc_cess_amount'] += d.amount
for invoice, values in amount_map.items():
frappe.db.set_value('Purchase Invoice', invoice, {
'itc_integrated_tax': values.get('itc_integrated_tax'),
'itc_central_tax': values.get('itc_central_tax'),
'itc_state_tax': values['itc_state_tax'],
'itc_cess_amount': values['itc_cess_amount'],
})

View File

@ -145,8 +145,8 @@ class Timesheet(Document):
data.project = data.project or frappe.db.get_value("Task", data.task, "project") data.project = data.project or frappe.db.get_value("Task", data.task, "project")
def validate_project(self, data): def validate_project(self, data):
if self.parent_project != data.project: if self.parent_project and self.parent_project != data.project:
frappe.throw(_("Row {0}: Poject must be same as {1}.")).format(data.idx, self.parent_project) frappe.throw(_("Row {0}: Project must be same as the one set in the Timesheet: {1}.")).format(data.idx, self.parent_project)
def validate_overlap_for(self, fieldname, args, value, ignore_validation=False): def validate_overlap_for(self, fieldname, args, value, ignore_validation=False):
if not value or ignore_validation: if not value or ignore_validation:
@ -208,27 +208,25 @@ class Timesheet(Document):
ts_detail.billing_rate = 0.0 ts_detail.billing_rate = 0.0
@frappe.whitelist() @frappe.whitelist()
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None, currency=None): def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None):
condition = field = join = '' condition = ''
if project:
condition += "and tsd.project = %(project)s"
if parent: if parent:
condition = "AND tsd.parent = %(parent)s" condition += "AND tsd.parent = %(parent)s"
if from_time and to_time: if from_time and to_time:
condition += "AND CAST(tsd.from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s" condition += "AND CAST(tsd.from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s"
if currency:
field = ", ts.currency as currency"
join = " INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent "
condition += " AND ts.currency = %(currency)s"
return frappe.db.sql("""SELECT tsd.name as name, return frappe.db.sql("""SELECT tsd.name as name,
tsd.parent as parent, tsd.billing_hours as billing_hours, tsd.parent as parent, tsd.billing_hours as billing_hours,
tsd.billing_amount as billing_amount, tsd.activity_type as activity_type, tsd.billing_amount as billing_amount, tsd.activity_type as activity_type,
tsd.description as description {0} tsd.description as description, ts.currency as currency
FROM `tabTimesheet Detail` tsd FROM `tabTimesheet Detail` tsd
{1} where tsd.parenttype = 'Timesheet' INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent
and tsd.docstatus=1 WHERE tsd.parenttype = 'Timesheet'
and tsd.project = %(project)s {2} and tsd.docstatus=1 {0}
and tsd.is_billable = 1 and tsd.is_billable = 1
and tsd.sales_invoice is null""".format(field, join, condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time, 'currency': currency}, as_dict=1) and tsd.sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs

View File

@ -1329,7 +1329,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
this.toggle_item_grid_columns(company_currency); this.toggle_item_grid_columns(company_currency);
if(this.frm.fields_dict["operations"]) { if (this.frm.doc.operations && this.frm.doc.operations.length > 0) {
this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations"); this.frm.set_currency_labels(["operating_cost", "hour_rate"], this.frm.doc.currency, "operations");
this.frm.set_currency_labels(["base_operating_cost", "base_hour_rate"], company_currency, "operations"); this.frm.set_currency_labels(["base_operating_cost", "base_hour_rate"], company_currency, "operations");
@ -1340,7 +1340,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}); });
} }
if(this.frm.fields_dict["scrap_items"]) { if (this.frm.doc.scrap_items && this.frm.doc.scrap_items.length > 0) {
this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "scrap_items"); this.frm.set_currency_labels(["rate", "amount"], this.frm.doc.currency, "scrap_items");
this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "scrap_items"); this.frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "scrap_items");
@ -1351,13 +1351,13 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
}); });
} }
if(this.frm.fields_dict["taxes"]) { if (this.frm.doc.taxes && this.frm.doc.taxes.length > 0) {
this.frm.set_currency_labels(["tax_amount", "total", "tax_amount_after_discount"], this.frm.doc.currency, "taxes"); this.frm.set_currency_labels(["tax_amount", "total", "tax_amount_after_discount"], this.frm.doc.currency, "taxes");
this.frm.set_currency_labels(["base_tax_amount", "base_total", "base_tax_amount_after_discount"], company_currency, "taxes"); this.frm.set_currency_labels(["base_tax_amount", "base_total", "base_tax_amount_after_discount"], company_currency, "taxes");
} }
if(this.frm.fields_dict["advances"]) { if (this.frm.doc.advances && this.frm.doc.advances.length > 0) {
this.frm.set_currency_labels(["advance_amount", "allocated_amount"], this.frm.set_currency_labels(["advance_amount", "allocated_amount"],
this.frm.doc.party_account_currency, "advances"); this.frm.doc.party_account_currency, "advances");
} }

View File

@ -74,9 +74,18 @@ erpnext.SerialNoBatchSelector = Class.extend({
fieldname: 'qty', fieldname: 'qty',
fieldtype:'Float', fieldtype:'Float',
read_only: me.has_batch && !me.has_serial_no, read_only: me.has_batch && !me.has_serial_no,
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'), label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
default: flt(me.item.stock_qty), default: flt(me.item.stock_qty),
}, },
...get_pending_qty_fields(me),
{
fieldname: 'uom',
read_only: 1,
fieldtype: 'Link',
options: 'UOM',
label: __('UOM'),
default: me.item.uom
},
{ {
fieldname: 'auto_fetch_button', fieldname: 'auto_fetch_button',
fieldtype:'Button', fieldtype:'Button',
@ -173,6 +182,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
if (this.has_batch && !this.has_serial_no) { if (this.has_batch && !this.has_serial_no) {
this.update_total_qty(); this.update_total_qty();
this.update_pending_qtys();
} }
this.dialog.show(); this.dialog.show();
@ -313,7 +323,21 @@ erpnext.SerialNoBatchSelector = Class.extend({
qty_field.set_input(total_qty); qty_field.set_input(total_qty);
}, },
update_pending_qtys: function() {
const pending_qty_field = this.dialog.fields_dict.pending_qty;
const total_selected_qty_field = this.dialog.fields_dict.total_selected_qty;
if (!pending_qty_field || !total_selected_qty_field) return;
const me = this;
const required_qty = this.dialog.fields_dict.required_qty.value;
const selected_qty = this.dialog.fields_dict.qty.value;
const total_selected_qty = selected_qty + calc_total_selected_qty(me);
const pending_qty = required_qty - total_selected_qty;
pending_qty_field.set_input(pending_qty);
total_selected_qty_field.set_input(total_selected_qty);
},
get_batch_fields: function() { get_batch_fields: function() {
var me = this; var me = this;
@ -415,6 +439,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
} }
me.update_total_qty(); me.update_total_qty();
me.update_pending_qtys();
} }
}, },
], ],
@ -511,3 +536,60 @@ erpnext.SerialNoBatchSelector = Class.extend({
]; ];
} }
}); });
function get_pending_qty_fields(me) {
if (!check_can_calculate_pending_qty(me)) return [];
const { frm: { doc: { fg_completed_qty }}, item: { item_code, stock_qty }} = me;
const { qty_consumed_per_unit } = erpnext.stock.bom.items[item_code];
const total_selected_qty = calc_total_selected_qty(me);
const required_qty = flt(fg_completed_qty) * flt(qty_consumed_per_unit);
const pending_qty = required_qty - (flt(stock_qty) + total_selected_qty);
const pending_qty_fields = [
{ fieldtype: 'Section Break', label: __('Pending Quantity') },
{
fieldname: 'required_qty',
read_only: 1,
fieldtype: 'Float',
label: __('Required Qty'),
default: required_qty
},
{ fieldtype: 'Column Break' },
{
fieldname: 'total_selected_qty',
read_only: 1,
fieldtype: 'Float',
label: __('Total Selected Qty'),
default: total_selected_qty
},
{ fieldtype: 'Column Break' },
{
fieldname: 'pending_qty',
read_only: 1,
fieldtype: 'Float',
label: __('Pending Qty'),
default: pending_qty
},
];
return pending_qty_fields;
}
function calc_total_selected_qty(me) {
const { frm: { doc: { items }}, item: { name, item_code }} = me;
const totalSelectedQty = items
.filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
.map( item => flt(item.qty) )
.reduce( (i, j) => i + j, 0);
return totalSelectedQty;
}
function check_can_calculate_pending_qty(me) {
const { frm: { doc }, item } = me;
const docChecks = doc.bom_no
&& doc.fg_completed_qty
&& erpnext.stock.bom
&& erpnext.stock.bom.name === doc.bom_no;
const itemChecks = !!item;
return docChecks && itemChecks;
}

View File

@ -129,11 +129,20 @@
@extend .pointer-no-select; @extend .pointer-no-select;
border-radius: var(--border-radius-md); border-radius: var(--border-radius-md);
box-shadow: var(--shadow-base); box-shadow: var(--shadow-base);
position: relative;
&:hover { &:hover {
transform: scale(1.02, 1.02); transform: scale(1.02, 1.02);
} }
.item-qty-pill {
position: absolute;
display: flex;
margin: var(--margin-sm);
justify-content: flex-end;
right: 0px;
}
.item-display { .item-display {
display: flex; display: flex;
align-items: center; align-items: center;
@ -766,9 +775,10 @@
> .payment-modes { > .payment-modes {
display: flex; display: flex;
padding-bottom: var(--padding-sm); padding-bottom: var(--padding-sm);
margin-bottom: var(--margin-xs); margin-bottom: var(--margin-sm);
overflow-x: scroll; overflow-x: scroll;
overflow-y: hidden; overflow-y: hidden;
flex-shrink: 0;
> .payment-mode-wrapper { > .payment-mode-wrapper {
min-width: 40%; min-width: 40%;
@ -825,9 +835,24 @@
> .fields-numpad-container { > .fields-numpad-container {
display: flex; display: flex;
flex: 1; flex: 1;
height: 100%;
position: relative;
justify-content: flex-end;
> .fields-section { > .fields-section {
flex: 1; flex: 1;
position: absolute;
display: flex;
flex-direction: column;
width: 50%;
height: 100%;
top: 0;
left: 0;
padding-bottom: var(--margin-md);
.invoice-fields {
overflow-y: scroll;
}
} }
> .number-pad { > .number-pad {
@ -835,6 +860,7 @@
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;
align-items: flex-end; align-items: flex-end;
max-width: 50%;
.numpad-container { .numpad-container {
display: grid; display: grid;
@ -861,6 +887,7 @@
margin-bottom: var(--margin-sm); margin-bottom: var(--margin-sm);
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
flex-shrink: 0;
> .totals { > .totals {
display: flex; display: flex;

View File

@ -172,7 +172,7 @@
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td><b>(A) {{__("ITC Available (whether in full op part)")}}</b></td> <td><b>(A) {{__("ITC Available (whether in full or part)")}}</b></td>
<td></td> <td></td>
<td></td> <td></td>
<td></td> <td></td>

View File

@ -3,148 +3,21 @@
# For license information, please see license.txt # For license information, please see license.txt
from __future__ import unicode_literals from __future__ import unicode_literals
import os
import json
import frappe import frappe
from six import iteritems
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
import json from frappe.utils import flt, cstr
from six import iteritems
from frappe.utils import flt, getdate
from erpnext.regional.india import state_numbers from erpnext.regional.india import state_numbers
class GSTR3BReport(Document): class GSTR3BReport(Document):
def before_save(self): def validate(self):
self.get_data() self.get_data()
def get_data(self): def get_data(self):
self.report_dict = json.loads(get_json('gstr_3b_report_template'))
self.report_dict = {
"gstin": "",
"ret_period": "",
"inward_sup": {
"isup_details": [
{
"ty": "GST",
"intra": 0,
"inter": 0
},
{
"ty": "NONGST",
"inter": 0,
"intra": 0
}
]
},
"sup_details": {
"osup_zero": {
"csamt": 0,
"txval": 0,
"iamt": 0
},
"osup_nil_exmp": {
"txval": 0
},
"osup_det": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"isup_rev": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"osup_nongst": {
"txval": 0,
}
},
"inter_sup": {
"unreg_details": [],
"comp_details": [],
"uin_details": []
},
"itc_elg": {
"itc_avl": [
{
"csamt": 0,
"samt": 0,
"ty": "IMPG",
"camt": 0,
"iamt": 0
},
{
"csamt": 0,
"samt": 0,
"ty": "IMPS",
"camt": 0,
"iamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "ISRC",
"camt": 0,
"iamt": 0
},
{
"ty": "ISD",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "OTH",
"camt": 0,
"iamt": 0
}
],
"itc_rev": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
],
"itc_net": {
"samt": 0,
"csamt": 0,
"camt": 0,
"iamt": 0
},
"itc_inelg": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
]
}
}
self.gst_details = self.get_company_gst_details() self.gst_details = self.get_company_gst_details()
self.report_dict["gstin"] = self.gst_details.get("gstin") self.report_dict["gstin"] = self.gst_details.get("gstin")
@ -152,23 +25,19 @@ class GSTR3BReport(Document):
self.month_no = get_period(self.month) self.month_no = get_period(self.month)
self.account_heads = self.get_account_heads() self.account_heads = self.get_account_heads()
outward_supply_tax_amounts = self.get_tax_amounts("Sales Invoice") self.get_outward_supply_details("Sales Invoice")
inward_supply_tax_amounts = self.get_tax_amounts("Purchase Invoice", reverse_charge="Y") self.set_outward_taxable_supplies()
self.get_outward_supply_details("Purchase Invoice", reverse_charge=True)
self.set_supplies_liable_to_reverse_charge()
itc_details = self.get_itc_details() itc_details = self.get_itc_details()
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y")
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
self.set_itc_details(itc_details) self.set_itc_details(itc_details)
self.get_itc_reversal_entries()
inter_state_supplies = self.get_inter_state_supplies(self.gst_details.get("gst_state_number"))
inward_nil_exempt = self.get_inward_nil_exempt(self.gst_details.get("gst_state")) inward_nil_exempt = self.get_inward_nil_exempt(self.gst_details.get("gst_state"))
self.set_inter_state_supply(inter_state_supplies)
self.set_inward_nil_exempt(inward_nil_exempt) self.set_inward_nil_exempt(inward_nil_exempt)
self.missing_field_invoices = self.get_missing_field_invoices() self.missing_field_invoices = self.get_missing_field_invoices()
self.json_output = frappe.as_json(self.report_dict) self.json_output = frappe.as_json(self.report_dict)
def set_inward_nil_exempt(self, inward_nil_exempt): def set_inward_nil_exempt(self, inward_nil_exempt):
@ -178,189 +47,95 @@ class GSTR3BReport(Document):
self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2) self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2)
def set_itc_details(self, itc_details): def set_itc_details(self, itc_details):
itc_eligible_type_map = {
itc_type_map = {
'IMPG': 'Import Of Capital Goods', 'IMPG': 'Import Of Capital Goods',
'IMPS': 'Import Of Service', 'IMPS': 'Import Of Service',
'ISRC': 'ITC on Reverse Charge',
'ISD': 'Input Service Distributor', 'ISD': 'Input Service Distributor',
'OTH': 'All Other ITC' 'OTH': 'All Other ITC'
} }
itc_ineligible_map = {
'RUL': 'Ineligible As Per Section 17(5)',
'OTH': 'Ineligible Others'
}
net_itc = self.report_dict["itc_elg"]["itc_net"] net_itc = self.report_dict["itc_elg"]["itc_net"]
for d in self.report_dict["itc_elg"]["itc_avl"]: for d in self.report_dict["itc_elg"]["itc_avl"]:
itc_type = itc_eligible_type_map.get(d["ty"])
itc_type = itc_type_map.get(d["ty"])
if d["ty"] == 'ISRC':
reverse_charge = ["Y"]
itc_type = 'All Other ITC'
gst_category = ['Unregistered', 'Overseas']
else:
gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
reverse_charge = ["N", "Y"]
for account_head in self.account_heads:
for category in gst_category:
for charge_type in reverse_charge:
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
for key in ['iamt', 'camt', 'samt', 'csamt']: for key in ['iamt', 'camt', 'samt', 'csamt']:
d[key] = flt(itc_details.get(itc_type, {}).get(key))
net_itc[key] += flt(d[key], 2) net_itc[key] += flt(d[key], 2)
for account_head in self.account_heads: for d in self.report_dict["itc_elg"]["itc_inelg"]:
itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1] itc_type = itc_ineligible_map.get(d["ty"])
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]: for key in ['iamt', 'camt', 'samt', 'csamt']:
itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2) d[key] = flt(itc_details.get(itc_type, {}).get(key))
def prepare_data(self, doctype, tax_details, supply_type, supply_category, gst_category_list, reverse_charge="N"): def get_itc_reversal_entries(self):
reversal_entries = frappe.db.sql("""
SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
where j.docstatus = 1
and j.is_opening = 'No'
and ja.parent = j.name
and j.voucher_type = 'Reversal Of ITC'
and month(j.posting_date) = %s and year(j.posting_date) = %s
and j.company = %s and j.company_gstin = %s
GROUP BY ja.account, j.reversal_type""", (self.month_no, self.year, self.company,
self.gst_details.get("gstin")), as_dict=1)
account_map = { net_itc = self.report_dict["itc_elg"]["itc_net"]
'sgst_account': 'samt',
'cess_account': 'csamt',
'cgst_account': 'camt',
'igst_account': 'iamt'
}
txval = 0 for entry in reversal_entries:
total_taxable_value = self.get_total_taxable_value(doctype, reverse_charge) if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules':
index = 0
else:
index = 1
for gst_category in gst_category_list: for key in ['camt', 'samt', 'iamt', 'csamt']:
txval += total_taxable_value.get(gst_category,0) if entry.account in self.account_heads.get(key):
for account_head in self.account_heads: self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount)
for account_type, account_name in iteritems(account_head): net_itc[key] -= flt(entry.amount)
if account_map.get(account_type) in self.report_dict.get(supply_type).get(supply_category):
self.report_dict[supply_type][supply_category][account_map.get(account_type)] += \
flt(tax_details.get((account_name, gst_category), {}).get("amount"), 2)
self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2)
def set_inter_state_supply(self, inter_state_supply):
osup_det = self.report_dict["sup_details"]["osup_det"]
for key, value in iteritems(inter_state_supply):
if key[0] == "Unregistered":
self.report_dict["inter_sup"]["unreg_details"].append(value)
if key[0] == "Registered Composition":
self.report_dict["inter_sup"]["comp_details"].append(value)
if key[0] == "UIN Holders":
self.report_dict["inter_sup"]["uin_details"].append(value)
def get_total_taxable_value(self, doctype, reverse_charge):
return frappe._dict(frappe.db.sql("""
select gst_category, sum(net_total) as total
from `tab{doctype}`
where docstatus = 1 and month(posting_date) = %s
and year(posting_date) = %s and reverse_charge = %s
and company = %s and company_gstin = %s
group by gst_category
""" #nosec
.format(doctype = doctype), (self.month_no, self.year, reverse_charge, self.company, self.gst_details.get("gstin"))))
def get_itc_details(self): def get_itc_details(self):
itc_amount = frappe.db.sql(""" itc_amounts = frappe.db.sql("""
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax,
t.account_head, s.eligibility_for_itc, s.reverse_charge sum(itc_central_tax) as itc_central_tax,
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t sum(itc_state_tax) as itc_state_tax,
where s.docstatus = 1 and t.parent = s.name sum(itc_cess_amount) as itc_cess_amount
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s FROM `tabPurchase Invoice`
and s.company_gstin = %s WHERE docstatus = 1
group by t.account_head, s.gst_category, s.eligibility_for_itc and is_opening = 'No'
""", and month(posting_date) = %s and year(posting_date) = %s and company = %s
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) and company_gstin = %s
GROUP BY eligibility_for_itc
""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
itc_details = {} itc_details = {}
for d in itc_amounts:
for d in itc_amount: itc_details.setdefault(d.eligibility_for_itc, {
itc_details.setdefault((d.gst_category, d.eligibility_for_itc, d.reverse_charge, d.account_head),{ 'iamt': d.itc_integrated_tax,
"amount": d.tax_amount 'camt': d.itc_central_tax,
'samt': d.itc_state_tax,
'csamt': d.itc_cess_amount
}) })
return itc_details return itc_details
def get_nil_rated_supply_value(self):
return frappe.db.sql("""
select sum(i.base_amount) as total from
`tabSales Invoice Item` i, `tabSales Invoice` s
where s.docstatus = 1 and i.parent = s.name and i.is_nil_exempt = 1
and month(s.posting_date) = %s and year(s.posting_date) = %s
and s.company = %s and s.company_gstin = %s""",
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)[0].total
def get_inter_state_supplies(self, state_number):
inter_state_supply_tax = frappe.db.sql(""" select t.account_head, t.tax_amount_after_discount_amount as tax_amount,
s.name, s.net_total, s.place_of_supply, s.gst_category from `tabSales Invoice` s, `tabSales Taxes and Charges` t
where t.parent = s.name and s.docstatus = 1 and month(s.posting_date) = %s and year(s.posting_date) = %s
and s.company = %s and s.company_gstin = %s and s.gst_category in ('Unregistered', 'Registered Composition', 'UIN Holders')
""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inter_state_supply_tax_mapping = {}
inter_state_supply_details = {}
for d in inter_state_supply_tax:
inter_state_supply_tax_mapping.setdefault(d.name, {
'place_of_supply': d.place_of_supply,
'taxable_value': d.net_total,
'gst_category': d.gst_category,
'camt': 0.0,
'samt': 0.0,
'iamt': 0.0,
'csamt': 0.0
})
if d.account_head in [a.cgst_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['camt'] += d.tax_amount
if d.account_head in [a.sgst_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['samt'] += d.tax_amount
if d.account_head in [a.igst_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['iamt'] += d.tax_amount
if d.account_head in [a.cess_account for a in self.account_heads]:
inter_state_supply_tax_mapping[d.name]['csamt'] += d.tax_amount
for key, value in iteritems(inter_state_supply_tax_mapping):
if value.get('place_of_supply'):
osup_det = self.report_dict["sup_details"]["osup_det"]
osup_det["txval"] = flt(osup_det["txval"] + value['taxable_value'], 2)
osup_det["iamt"] = flt(osup_det["iamt"] + value['iamt'], 2)
osup_det["camt"] = flt(osup_det["camt"] + value['camt'], 2)
osup_det["samt"] = flt(osup_det["samt"] + value['samt'], 2)
osup_det["csamt"] = flt(osup_det["csamt"] + value['csamt'], 2)
if state_number != value.get('place_of_supply').split("-")[0]:
inter_state_supply_details.setdefault((value.get('gst_category'), value.get('place_of_supply')), {
"txval": 0.0,
"pos": value.get('place_of_supply').split("-")[0],
"iamt": 0.0
})
inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['txval'] += value['taxable_value']
inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['iamt'] += value['iamt']
return inter_state_supply_details
def get_inward_nil_exempt(self, state): def get_inward_nil_exempt(self, state):
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount, inward_nil_exempt = frappe.db.sql("""
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
where p.docstatus = 1 and p.name = i.parent FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
WHERE p.docstatus = 1 and p.name = i.parent
and p.is_opening = 'No'
and p.gst_category != 'Registered Composition' and p.gst_category != 'Registered Composition'
and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s month(p.posting_date) = %s and year(p.posting_date) = %s
group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1) and p.company = %s and p.company_gstin = %s
GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
FROM `tabPurchase Invoice`
WHERE docstatus = 1 and gst_category = 'Registered Composition'
and month(posting_date) = %s and year(posting_date) = %s
and company = %s and company_gstin = %s
group by place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = { inward_nil_exempt_details = {
"gst": { "gst": {
@ -388,37 +163,193 @@ class GSTR3BReport(Document):
return inward_nil_exempt_details return inward_nil_exempt_details
def get_tax_amounts(self, doctype, reverse_charge="N"): def get_outward_supply_details(self, doctype, reverse_charge=None):
self.get_outward_tax_invoices(doctype, reverse_charge=reverse_charge)
self.get_outward_items(doctype)
self.get_outward_tax_details(doctype)
def get_outward_tax_invoices(self, doctype, reverse_charge=None):
self.invoices = []
self.invoice_detail_map = {}
condition = ''
if reverse_charge:
condition += "AND reverse_charge = 'Y'"
invoice_details = frappe.db.sql("""
SELECT
name, gst_category, export_type, place_of_supply
FROM
`tab{doctype}`
WHERE
docstatus = 1
AND month(posting_date) = %s
AND year(posting_date) = %s
AND company = %s
AND company_gstin = %s
AND is_opening = 'No'
{reverse_charge}
ORDER BY name
""".format(doctype=doctype, reverse_charge=condition), (self.month_no, self.year,
self.company, self.gst_details.get("gstin")), as_dict=1)
for d in invoice_details:
self.invoice_detail_map.setdefault(d.name, d)
self.invoices.append(d.name)
def get_outward_items(self, doctype):
self.invoice_items = frappe._dict()
self.is_nil_exempt = []
self.is_non_gst = []
if self.get('invoices'):
item_details = frappe.db.sql("""
SELECT
item_code, parent, taxable_value, base_net_amount, item_tax_rate,
is_nil_exempt, is_non_gst
FROM
`tab%s Item`
WHERE parent in (%s)
""" % (doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
for d in item_details:
if d.item_code not in self.invoice_items.get(d.parent, {}):
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details
if i.item_code == d.item_code and i.parent == d.parent))
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
self.is_nil_exempt.append(d.item_code)
if d.is_non_gst and d.item_code not in self.is_non_gst:
self.is_non_gst.append(d.item_code)
def get_outward_tax_details(self, doctype):
if doctype == "Sales Invoice": if doctype == "Sales Invoice":
tax_template = 'Sales Taxes and Charges' tax_template = 'Sales Taxes and Charges'
elif doctype == "Purchase Invoice": elif doctype == "Purchase Invoice":
tax_template = 'Purchase Taxes and Charges' tax_template = 'Purchase Taxes and Charges'
tax_amounts = frappe.db.sql(""" self.items_based_on_tax_rate = {}
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head self.invoice_cess = frappe._dict()
from `tab{doctype}` s , `tab{template}` t self.cgst_sgst_invoices = []
where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
and s.company_gstin = %s
group by t.account_head, s.gst_category
""" #nosec
.format(doctype=doctype, template=tax_template),
(reverse_charge, self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
tax_details = {} if self.get('invoices'):
tax_details = frappe.db.sql("""
SELECT
parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
FROM `tab%s`
WHERE
parenttype = %s and docstatus = 1
and parent in (%s)
ORDER BY account_head
""" % (tax_template, '%s', ', '.join(['%s']*len(self.invoices))),
tuple([doctype] + list(self.invoices)))
for d in tax_amounts: for parent, account, item_wise_tax_detail, tax_amount in tax_details:
tax_details.setdefault( if account in self.account_heads.get('csamt'):
(d.account_head,d.gst_category),{ self.invoice_cess.setdefault(parent, tax_amount)
"amount": d.get("tax_amount"), else:
} if item_wise_tax_detail:
) try:
item_wise_tax_detail = json.loads(item_wise_tax_detail)
cgst_or_sgst = False
if account in self.account_heads.get('camt') \
or account in self.account_heads.get('samt'):
cgst_or_sgst = True
return tax_details for item_code, tax_amounts in item_wise_tax_detail.items():
if not (cgst_or_sgst or account in self.account_heads.get('iamt') or
(item_code in self.is_non_gst + self.is_nil_exempt)):
continue
tax_rate = tax_amounts[0]
if tax_rate:
if cgst_or_sgst:
tax_rate *= 2
if parent not in self.cgst_sgst_invoices:
self.cgst_sgst_invoices.append(parent)
rate_based_dict = self.items_based_on_tax_rate\
.setdefault(parent, {}).setdefault(tax_rate, [])
if item_code not in rate_based_dict:
rate_based_dict.append(item_code)
except ValueError:
continue
if self.get('invoice_items'):
# Build itemised tax for export invoices, nil and exempted where tax table is blank
for invoice, items in iteritems(self.invoice_items):
if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type')
== "Without Payment of Tax"):
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
def set_outward_taxable_supplies(self):
inter_state_supply_details = {}
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items:
if item_code in self.is_nil_exempt:
self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
elif item_code in self.is_non_gst:
self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
elif rate == 0:
self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
#self.report_dict['sup_details']['osup_zero'][key] += tax_amount
else:
if inv in self.cgst_sgst_invoices:
tax_rate = rate/2
self.report_dict['sup_details']['osup_det']['camt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['osup_det']['samt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
else:
self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply', '00-Other Territory')
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:
inter_state_supply_details.setdefault((gst_category, place_of_supply), {
"txval": 0.0,
"pos": place_of_supply.split("-")[0],
"iamt": 0.0
})
inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value
inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
self.set_inter_state_supply(inter_state_supply_details)
def set_supplies_liable_to_reverse_charge(self):
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
for rate, items in items_based_on_rate.items():
for item_code, taxable_value in self.invoice_items.get(inv).items():
if item_code in items:
if inv in self.cgst_sgst_invoices:
tax_rate = rate/2
self.report_dict['sup_details']['isup_rev']['camt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['isup_rev']['samt'] += (taxable_value * tax_rate /100)
self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
else:
self.report_dict['sup_details']['isup_rev']['iamt'] += (taxable_value * rate /100)
self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
def set_inter_state_supply(self, inter_state_supply):
for key, value in iteritems(inter_state_supply):
if key[0] == "Unregistered":
self.report_dict["inter_sup"]["unreg_details"].append(value)
if key[0] == "Registered Composition":
self.report_dict["inter_sup"]["comp_details"].append(value)
if key[0] == "UIN Holders":
self.report_dict["inter_sup"]["uin_details"].append(value)
def get_company_gst_details(self): def get_company_gst_details(self):
gst_details = frappe.get_all("Address", gst_details = frappe.get_all("Address",
fields=["gstin", "gst_state", "gst_state_number"], fields=["gstin", "gst_state", "gst_state_number"],
filters={ filters={
@ -431,20 +362,28 @@ class GSTR3BReport(Document):
frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address)) frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address))
def get_account_heads(self): def get_account_heads(self):
account_map = {
'sgst_account': 'samt',
'cess_account': 'csamt',
'cgst_account': 'camt',
'igst_account': 'iamt'
}
account_heads = frappe.get_all("GST Account", account_heads = {}
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"], gst_settings_accounts = frappe.get_all("GST Account",
filters={ filters={'company': self.company, 'is_reverse_charge_account': 0},
"company":self.company fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
})
if account_heads: if not gst_settings_accounts:
return account_heads frappe.throw(_("Please set GST Accounts in GST Settings"))
else:
frappe.throw(_("Please set account heads in GST Settings for Compnay {0}").format(self.company)) for d in gst_settings_accounts:
for acc, val in d.items():
account_heads.setdefault(account_map.get(acc), []).append(val)
return account_heads
def get_missing_field_invoices(self): def get_missing_field_invoices(self):
missing_field_invoices = [] missing_field_invoices = []
for doctype in ["Sales Invoice", "Purchase Invoice"]: for doctype in ["Sales Invoice", "Purchase Invoice"]:
@ -456,26 +395,32 @@ class GSTR3BReport(Document):
party_type = 'Supplier' party_type = 'Supplier'
party = 'supplier' party = 'supplier'
docnames = frappe.db.sql(""" docnames = frappe.db.sql(
select t1.name from `tab{doctype}` t1, `tab{party_type}` t2 """
where t1.docstatus = 1 and month(t1.posting_date) = %s and year(t1.posting_date) = %s SELECT t1.name FROM `tab{doctype}` t1, `tab{party_type}` t2
WHERE t1.docstatus = 1 and t1.is_opening = 'No'
and month(t1.posting_date) = %s and year(t1.posting_date) = %s
and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and
t2.gst_category != 'Overseas' t2.gst_category != 'Overseas'
""".format(doctype = doctype, party_type = party_type, party=party), (self.month_no, self.year, self.company), as_dict=1) #nosec """.format(doctype = doctype, party_type = party_type,
party=party) ,(self.month_no, self.year, self.company), as_dict=1) #nosec
for d in docnames: for d in docnames:
missing_field_invoices.append(d.name) missing_field_invoices.append(d.name)
return ",".join(missing_field_invoices) return ",".join(missing_field_invoices)
def get_state_code(state): def get_json(template):
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
with open(file_path, 'r') as f:
return cstr(f.read())
def get_state_code(state):
state_code = state_numbers.get(state) state_code = state_numbers.get(state)
return state_code return state_code
def get_period(month, year=None): def get_period(month, year=None):
month_no = { month_no = {
"January": 1, "January": 1,
"February": 2, "February": 2,
@ -499,13 +444,11 @@ def get_period(month, year=None):
@frappe.whitelist() @frappe.whitelist()
def view_report(name): def view_report(name):
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output') json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
return json.loads(json_data) return json.loads(json_data)
@frappe.whitelist() @frappe.whitelist()
def make_json(name): def make_json(name):
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output') json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
file_name = "GST3B.json" file_name = "GST3B.json"
frappe.local.response.filename = file_name frappe.local.response.filename = file_name

View File

@ -0,0 +1,127 @@
{
"gstin": "",
"ret_period": "",
"inward_sup": {
"isup_details": [
{
"ty": "GST",
"intra": 0,
"inter": 0
},
{
"ty": "NONGST",
"inter": 0,
"intra": 0
}
]
},
"sup_details": {
"osup_zero": {
"csamt": 0,
"txval": 0,
"iamt": 0
},
"osup_nil_exmp": {
"txval": 0
},
"osup_det": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"isup_rev": {
"samt": 0,
"csamt": 0,
"txval": 0,
"camt": 0,
"iamt": 0
},
"osup_nongst": {
"txval": 0
}
},
"inter_sup": {
"unreg_details": [],
"comp_details": [],
"uin_details": []
},
"itc_elg": {
"itc_avl": [
{
"csamt": 0,
"samt": 0,
"ty": "IMPG",
"camt": 0,
"iamt": 0
},
{
"csamt": 0,
"samt": 0,
"ty": "IMPS",
"camt": 0,
"iamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "ISRC",
"camt": 0,
"iamt": 0
},
{
"ty": "ISD",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"samt": 0,
"csamt": 0,
"ty": "OTH",
"camt": 0,
"iamt": 0
}
],
"itc_rev": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
],
"itc_net": {
"samt": 0,
"csamt": 0,
"camt": 0,
"iamt": 0
},
"itc_inelg": [
{
"ty": "RUL",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
},
{
"ty": "OTH",
"iamt": 0,
"camt": 0,
"samt": 0,
"csamt": 0
}
]
}
}

View File

@ -60,8 +60,7 @@ class TestGSTR3BReport(unittest.TestCase):
output = json.loads(report.json_output) output = json.loads(report.json_output)
self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 36), self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 54)
self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18), self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100), self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250) self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250)

View File

@ -114,9 +114,12 @@ def add_print_formats():
def make_property_setters(patch=False): def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters # GST rules do not allow for an invoice no. bigger than 16 characters
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
if not patch: if not patch:
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True): def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC', hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@ -198,15 +201,20 @@ def make_custom_fields(update=True):
purchase_invoice_itc_fields = [ purchase_invoice_itc_fields = [
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC', dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1, fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nIneligible\nAll Other ITC', default="All Other ITC"), options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
default="All Other ITC"),
dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax', dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax',
fieldtype='Data', insert_after='eligibility_for_itc', print_hide=1), fieldtype='Currency', insert_after='eligibility_for_itc',
options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_central_tax', label='Availed ITC Central Tax', dict(fieldname='itc_central_tax', label='Availed ITC Central Tax',
fieldtype='Data', insert_after='itc_integrated_tax', print_hide=1), fieldtype='Currency', insert_after='itc_integrated_tax',
options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax', dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax',
fieldtype='Data', insert_after='itc_central_tax', print_hide=1), fieldtype='Currency', insert_after='itc_central_tax',
options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_cess_amount', label='Availed ITC Cess', dict(fieldname='itc_cess_amount', label='Availed ITC Cess',
fieldtype='Data', insert_after='itc_state_tax', print_hide=1), fieldtype='Currency', insert_after='itc_state_tax',
options='Company:company:default_currency', print_hide=1),
] ]
sales_invoice_gst_fields = [ sales_invoice_gst_fields = [
@ -236,6 +244,23 @@ def make_custom_fields(update=True):
depends_on="eval:doc.gst_category=='Overseas' "), depends_on="eval:doc.gst_category=='Overseas' "),
] ]
journal_entry_fields = [
dict(fieldname='reversal_type', label='Reversal Type',
fieldtype='Select', insert_after='voucher_type', print_hide=1,
options="As per rules 42 & 43 of CGST Rules\nOthers",
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_address', label='Company Address',
fieldtype='Link', options='Address', insert_after='reversal_type',
print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
dict(fieldname='company_gstin', label='Company GSTIN',
fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
fetch_from='company_address.gstin',
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
]
inter_state_gst_field = [ inter_state_gst_field = [
dict(fieldname='is_inter_state', label='Is Inter State', dict(fieldname='is_inter_state', label='Is Inter State',
fieldtype='Check', insert_after='disabled', print_hide=1), fieldtype='Check', insert_after='disabled', print_hide=1),
@ -430,13 +455,13 @@ def make_custom_fields(update=True):
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
print_hide=1, hidden=1), print_hide=1, hidden=1),
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
no_copy=1, print_hide=1), no_copy=1, print_hide=1),
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date', dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
no_copy=1, print_hide=1), no_copy=1, print_hide=1),
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
@ -469,6 +494,7 @@ def make_custom_fields(update=True):
'Purchase Receipt': purchase_invoice_gst_fields, 'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Journal Entry': journal_entry_fields,
'Sales Order': sales_invoice_gst_fields, 'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field, 'Tax Category': inter_state_gst_field,
'Item': [ 'Item': [
@ -486,7 +512,7 @@ def make_custom_fields(update=True):
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], 'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], 'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Salary Component': [ 'Salary Component': [
dict(fieldname= 'component_type', dict(fieldname= 'component_type',

View File

@ -204,8 +204,6 @@ def get_regional_address_details(party_details, doctype, company):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template" master_doctype = "Sales Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details) get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'): if party_details.get('taxes_and_charges'):
@ -216,7 +214,6 @@ def get_regional_address_details(party_details, doctype, company):
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template" master_doctype = "Purchase Taxes and Charges Template"
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details) get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'): if party_details.get('taxes_and_charges'):
@ -283,20 +280,6 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
{'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name') {'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
return default_tax return default_tax
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))},
['gst_category', 'export_type'], as_dict=1)
if gst_details:
if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax':
default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0,
"gst_state": number_state_mapping[party_details.company_gstin[:2]]})
party_details["taxes_and_charges"] = default_tax
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
def calculate_annual_eligible_hra_exemption(doc): def calculate_annual_eligible_hra_exemption(doc):
basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"]) basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"])
if not (basic_component and hra_component): if not (basic_component and hra_component):
@ -697,10 +680,19 @@ def validate_state_code(state_code, address):
return int(state_code) return int(state_code)
@frappe.whitelist() @frappe.whitelist()
def get_gst_accounts(company, account_wise=False): def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0):
filters={"parent": "GST Settings"}
if company:
filters.update({'company': company})
if only_reverse_charge:
filters.update({'is_reverse_charge_account': 1})
elif only_non_reverse_charge:
filters.update({'is_reverse_charge_account': 0})
gst_accounts = frappe._dict() gst_accounts = frappe._dict()
gst_settings_accounts = frappe.get_all("GST Account", gst_settings_accounts = frappe.get_all("GST Account",
filters={"parent": "GST Settings", "company": company}, filters=filters,
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
if not gst_settings_accounts and not frappe.flags.in_test: if not gst_settings_accounts and not frappe.flags.in_test:
@ -715,101 +707,63 @@ def get_gst_accounts(company, account_wise=False):
return gst_accounts return gst_accounts
def update_grand_total_for_rcm(doc, method): def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country') country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India': if country != 'India':
return return
gst_tax, base_gst_tax = get_gst_tax_amount(doc) base_gst_tax = 0
base_reverse_charge_booked = 0
if not base_gst_tax:
return
if doc.reverse_charge == 'Y': if doc.reverse_charge == 'Y':
doc.taxes_and_charges_added -= gst_tax gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1)
doc.total_taxes_and_charges -= gst_tax reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
doc.base_taxes_and_charges_added -= base_gst_tax + gst_accounts.get('igst_account')
doc.base_total_taxes_and_charges -= base_gst_tax
update_totals(gst_tax, base_gst_tax, doc) gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
def update_totals(gst_tax, base_gst_tax, doc):
doc.base_grand_total -= base_gst_tax
doc.grand_total -= gst_tax
if doc.meta.get_field("rounded_total"):
if doc.is_rounded_total_disabled():
doc.outstanding_amount = doc.grand_total
else:
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
doc.currency, doc.precision("rounded_total"))
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
doc.precision("rounding_adjustment"))
doc.outstanding_amount = doc.rounded_total or doc.grand_total
doc.in_words = money_in_words(doc.grand_total, doc.currency)
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
doc.set_payment_schedule()
def make_regional_gl_entries(gl_entries, doc):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return gl_entries
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return gl_entries
if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account') + gst_accounts.get('igst_account')
for tax in doc.get('taxes'): for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"): if tax.account_head in non_reverse_charge_accounts:
continue if tax.add_deduct_tax == 'Add':
base_gst_tax += tax.base_tax_amount_after_discount_amount
else:
base_gst_tax += tax.base_tax_amount_after_discount_amount
elif tax.account_head in reverse_charge_accounts:
if tax.add_deduct_tax == 'Add':
base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
else:
base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit" if base_gst_tax != base_reverse_charge_booked:
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: msg = _("Booked reverse charge is not equal to applied tax amount")
account_currency = get_account_currency(tax.account_head) msg += "<br>"
msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format(
gst_document_link='<a href="https://docs.erpnext.com/docs/user/manual/en/regional/india/gst-setup">GST Documentation</a>')
gl_entries.append(doc.get_gl_dict( frappe.throw(msg)
{
"account": tax.account_head,
"cost_center": tax.cost_center,
"posting_date": doc.posting_date,
"against": doc.supplier,
dr_or_cr: tax.base_tax_amount_after_discount_amount,
dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
if account_currency==doc.company_currency \
else tax.tax_amount_after_discount_amount
}, account_currency, item=tax)
)
return gl_entries def update_itc_availed_fields(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country')
def get_gst_tax_amount(doc): if country != 'India':
gst_accounts = get_gst_accounts(doc.company) return
gst_account_list = gst_accounts.get('cgst_account', []) + gst_accounts.get('sgst_account', []) \
+ gst_accounts.get('igst_account', [])
base_gst_tax = 0 # Initialize values
gst_tax = 0 doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0
gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
for tax in doc.get('taxes'): for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"): if tax.account_head in gst_accounts.get('igst_account', []):
continue doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('sgst_account', []):
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list: doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount)
base_gst_tax += tax.base_tax_amount_after_discount_amount if tax.account_head in gst_accounts.get('cgst_account', []):
gst_tax += tax.tax_amount_after_discount_amount doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('cess_account', []):
return gst_tax, base_gst_tax doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount)
@frappe.whitelist() @frappe.whitelist()
def get_regional_round_off_accounts(company, account_list): def get_regional_round_off_accounts(company, account_list):
@ -879,3 +833,24 @@ def update_taxable_values(doc, method):
if total_charges != additional_taxes: if total_charges != additional_taxes:
diff = additional_taxes - total_charges diff = additional_taxes - total_charges
doc.get('items')[item_count - 1].taxable_value += diff doc.get('items')[item_count - 1].taxable_value += diff
def get_depreciation_amount(asset, depreciable_value, row):
depreciation_left = flt(row.total_number_of_depreciations) - flt(asset.number_of_depreciations_booked)
if row.depreciation_method in ("Straight Line", "Manual"):
depreciation_amount = (flt(row.value_after_depreciation) -
flt(row.expected_value_after_useful_life)) / depreciation_left
else:
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation
if depreciable_value == asset.gross_purchase_amount:
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
diff = date_diff(asset.available_for_use_date, row.depreciation_start_date)
if diff <= 180:
rate_of_depreciation = rate_of_depreciation / 2
frappe.msgprint(
_('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))
return depreciation_amount

View File

@ -46,7 +46,13 @@ frappe.query_reports["GSTR-1"] = {
"label": __("Type of Business"), "label": __("Type of Business"),
"fieldtype": "Select", "fieldtype": "Select",
"reqd": 1, "reqd": 1,
"options": ["B2B", "B2C Large", "B2C Small", "CDNR", "EXPORT"], "options": [
{ "value": "B2B", "label": __("B2B Invoices - 4A, 4B, 4C, 6B, 6C") },
{ "value": "B2C Large", "label": __("B2C(Large) Invoices - 5A, 5B") },
{ "value": "B2C Small", "label": __("B2C(Small) Invoices - 7") },
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
{ "value": "EXPORT", "label": __("Export Invoice - 6A") }
],
"default": "B2B" "default": "B2B"
} }
], ],

View File

@ -32,6 +32,7 @@ class Gstr1Report(object):
reverse_charge, reverse_charge,
return_against, return_against,
is_return, is_return,
is_debit_note,
gst_category, gst_category,
export_type, export_type,
port_code, port_code,
@ -42,7 +43,7 @@ class Gstr1Report(object):
def run(self): def run(self):
self.get_columns() self.get_columns()
self.gst_accounts = get_gst_accounts(self.filters.company) self.gst_accounts = get_gst_accounts(self.filters.company, only_non_reverse_charge=1)
self.get_invoice_data() self.get_invoice_data()
if self.invoices: if self.invoices:
@ -62,9 +63,9 @@ class Gstr1Report(object):
for rate, items in items_based_on_rate.items(): for rate, items in items_based_on_rate.items():
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
if self.filters.get("type_of_business") == "CDNR": if self.filters.get("type_of_business") == "CDNR-REG":
row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N") row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N")
row.append("C" if invoice_details.return_against else "R") row.append("C" if invoice_details.is_return else "D")
if taxable_value: if taxable_value:
self.data.append(row) self.data.append(row)
@ -105,7 +106,7 @@ class Gstr1Report(object):
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items): def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
row = [] row = []
for fieldname in self.invoice_fields: for fieldname in self.invoice_fields:
if self.filters.get("type_of_business") == "CDNR" and fieldname == "invoice_value": if self.filters.get("type_of_business") == "CDNR-REG" and fieldname == "invoice_value":
row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total)) row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total))
elif fieldname == "invoice_value": elif fieldname == "invoice_value":
row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total) row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total)
@ -171,7 +172,7 @@ class Gstr1Report(object):
if self.filters.get("type_of_business") == "B2B": if self.filters.get("type_of_business") == "B2B":
conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1" conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"): if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit') b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@ -179,19 +180,19 @@ class Gstr1Report(object):
frappe.throw(_("Please set B2C Limit in GST Settings.")) frappe.throw(_("Please set B2C Limit in GST Settings."))
if self.filters.get("type_of_business") == "B2C Large": if self.filters.get("type_of_business") == "B2C Large":
conditions += """ and ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'') conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
and grand_total > {0} and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit)) AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "B2C Small": elif self.filters.get("type_of_business") == "B2C Small":
conditions += """ and ( conditions += """ AND (
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2) SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
or grand_total <= {0}) and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit)) OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "CDNR": elif self.filters.get("type_of_business") == "CDNR-REG":
conditions += """ and is_return = 1 """ conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')"""
elif self.filters.get("type_of_business") == "EXPORT": elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ and is_return !=1 and gst_category = 'Overseas' """ conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
return conditions return conditions
def get_invoice_items(self): def get_invoice_items(self):
@ -403,7 +404,7 @@ class Gstr1Report(object):
"width": 100 "width": 100
} }
] ]
elif self.filters.get("type_of_business") == "CDNR": elif self.filters.get("type_of_business") == "CDNR-REG":
self.invoice_columns = [ self.invoice_columns = [
{ {
"fieldname": "customer_gstin", "fieldname": "customer_gstin",
@ -437,6 +438,17 @@ class Gstr1Report(object):
"options": "Sales Invoice", "options": "Sales Invoice",
"width":120 "width":120
}, },
{
"fieldname": "reverse_charge",
"label": "Reverse Charge",
"fieldtype": "Data"
},
{
"fieldname": "export_type",
"label": "Export Type",
"fieldtype": "Data",
"hidden": 1
},
{ {
"fieldname": "reason_for_issuing_document", "fieldname": "reason_for_issuing_document",
"label": "Reason For Issuing document", "label": "Reason For Issuing document",
@ -449,6 +461,11 @@ class Gstr1Report(object):
"fieldtype": "Data", "fieldtype": "Data",
"width": 120 "width": 120
}, },
{
"fieldname": "gst_category",
"label": "GST Category",
"fieldtype": "Data"
},
{ {
"fieldname": "invoice_value", "fieldname": "invoice_value",
"label": "Invoice Value", "label": "Invoice Value",
@ -458,10 +475,10 @@ class Gstr1Report(object):
] ]
self.other_columns = [ self.other_columns = [
{ {
"fieldname": "cess_amount", "fieldname": "cess_amount",
"label": "Cess Amount", "label": "Cess Amount",
"fieldtype": "Currency", "fieldtype": "Currency",
"width": 100 "width": 100
}, },
{ {
"fieldname": "pre_gst", "fieldname": "pre_gst",
@ -589,6 +606,12 @@ def get_json(filters, report_name, data):
out = get_export_json(res) out = get_export_json(res)
gst_json["exp"] = out gst_json["exp"] = out
elif filters["type_of_business"] == 'CDNR-REG':
for item in report_data[:-1]:
res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
out = get_cdnr_reg_json(res, gstin)
gst_json["cdnr"] = out
return { return {
'report_name': report_name, 'report_name': report_name,
@ -628,7 +651,6 @@ def get_b2b_json(res, gstin):
return out return out
def get_b2cs_json(data, gstin): def get_b2cs_json(data, gstin):
company_state_number = gstin[0:2] company_state_number = gstin[0:2]
out = [] out = []
@ -713,6 +735,54 @@ def get_export_json(res):
return out return out
def get_cdnr_reg_json(res, gstin):
out = []
for gst_in in res:
cdnr_item, inv = {"ctin": gst_in, "nt": []}, []
if not gst_in: continue
for number, invoice in iteritems(res[gst_in]):
if not invoice[0]["place_of_supply"]:
frappe.throw(_("""{0} not entered in Invoice {1}.
Please update and try again""").format(frappe.bold("Place Of Supply"),
frappe.bold(invoice[0]['invoice_number'])))
inv_item = {
"nt_num": invoice[0]["invoice_number"],
"nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'),
"val": abs(flt(invoice[0]["invoice_value"])),
"ntty": invoice[0]["document_type"],
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
"rchrg": invoice[0]["reverse_charge"],
"inv_type": get_invoice_type_for_cdnr(invoice[0])
}
inv_item["itms"] = []
for item in invoice:
inv_item["itms"].append(get_rate_and_tax_details(item, gstin))
inv.append(inv_item)
if not inv: continue
cdnr_item["nt"] = inv
out.append(cdnr_item)
return out
def get_invoice_type_for_cdnr(row):
if row.get('gst_category') == 'SEZ':
if row.get('export_type') == 'WPAY':
invoice_type = 'SEWP'
else:
invoice_type = 'SEWOP'
elif row.get('gst_category') == 'Deemed Export':
row.invoice_type = 'DE'
elif row.get('gst_category') == 'Registered Regular':
invoice_type = 'R'
return invoice_type
def get_basic_invoice_detail(row): def get_basic_invoice_detail(row):
return { return {
"inum": row["invoice_number"], "inum": row["invoice_number"],

View File

@ -56,10 +56,6 @@ erpnext.PointOfSale.Controller = class {
dialog.fields_dict.balance_details.grid.refresh(); dialog.fields_dict.balance_details.grid.refresh();
}); });
} }
const pos_profile_query = {
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
filters: { company: frappe.defaults.get_default('company') }
}
const dialog = new frappe.ui.Dialog({ const dialog = new frappe.ui.Dialog({
title: __('Create POS Opening Entry'), title: __('Create POS Opening Entry'),
static: true, static: true,
@ -105,6 +101,10 @@ erpnext.PointOfSale.Controller = class {
primary_action_label: __('Submit') primary_action_label: __('Submit')
}); });
dialog.show(); dialog.show();
const pos_profile_query = {
query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
filters: { company: dialog.fields_dict.company.get_value() }
};
} }
async prepare_app_defaults(data) { async prepare_app_defaults(data) {

View File

@ -90,14 +90,16 @@ erpnext.PointOfSale.ItemSelector = class {
function get_item_image_html() { function get_item_image_html() {
if (!me.hide_images && item_image) { if (!me.hide_images && item_image) {
return `<div class="flex" style="margin: 8px; justify-content: flex-end"> return `<div class="item-qty-pill">
<span class="indicator-pill whitespace-nowrap ${indicator_color}" id="text">${qty_to_display}</span></div> <span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100"> <div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<img class="h-full" src="${item_image}" alt="${frappe.get_abbr(item.item_name)}" style="object-fit: cover;"> <img class="h-full" src="${item_image}" alt="${frappe.get_abbr(item.item_name)}" style="object-fit: cover;">
</div>`; </div>`;
} else { } else {
return `<div class="flex" style="margin: 8px; justify-content: flex-end"> return `<div class="item-qty-pill">
<span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span></div> <span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span>
</div>
<div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`; <div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`;
} }
} }

View File

@ -169,9 +169,9 @@ frappe.ui.form.on("Company", {
return; return;
} }
frappe.call({ frappe.call({
method: "erpnext.setup.doctype.company.delete_company_transactions.delete_company_transactions", method: "erpnext.setup.doctype.company.company.create_transaction_deletion_request",
args: { args: {
company_name: data.company_name company: data.company_name
}, },
freeze: true, freeze: true,
callback: function(r, rt) { callback: function(r, rt) {

View File

@ -613,4 +613,13 @@ def get_default_company_address(name, sort_key='is_primary_address', existing_ad
if out: if out:
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0] return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0]
else: else:
return None return None
@frappe.whitelist()
def create_transaction_deletion_request(company):
tdr = frappe.get_doc({
'doctype': 'Transaction Deletion Record',
'company': company
})
tdr.insert()
tdr.submit()

View File

@ -1,117 +0,0 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
import frappe
from frappe.utils import cint
from frappe import _
from frappe.desk.notifications import clear_notifications
import functools
@frappe.whitelist()
def delete_company_transactions(company_name):
frappe.only_for("System Manager")
doc = frappe.get_doc("Company", company_name)
if frappe.session.user != doc.owner and frappe.session.user != 'Administrator':
frappe.throw(_("Transactions can only be deleted by the creator of the Company"),
frappe.PermissionError)
delete_bins(company_name)
delete_lead_addresses(company_name)
for doctype in frappe.db.sql_list("""select parent from
tabDocField where fieldtype='Link' and options='Company'"""):
if doctype not in ("Account", "Cost Center", "Warehouse", "Budget",
"Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment", "Mode of Payment Account",
"Item Default", "Customer", "Supplier", "GST Account"):
delete_for_doctype(doctype, company_name)
# reset company values
doc.total_monthly_sales = 0
doc.sales_monthly_history = None
doc.save()
# Clear notification counts
clear_notifications()
def delete_for_doctype(doctype, company_name):
meta = frappe.get_meta(doctype)
company_fieldname = meta.get("fields", {"fieldtype": "Link",
"options": "Company"})[0].fieldname
if not meta.issingle:
if not meta.istable:
# delete communication
delete_communications(doctype, company_name, company_fieldname)
# delete children
for df in meta.get_table_fields():
frappe.db.sql("""delete from `tab{0}` where parent in
(select name from `tab{1}` where `{2}`=%s)""".format(df.options,
doctype, company_fieldname), company_name)
#delete version log
frappe.db.sql("""delete from `tabVersion` where ref_doctype=%s and docname in
(select name from `tab{0}` where `{1}`=%s)""".format(doctype,
company_fieldname), (doctype, company_name))
# delete parent
frappe.db.sql("""delete from `tab{0}`
where {1}= %s """.format(doctype, company_fieldname), company_name)
# reset series
naming_series = meta.get_field("naming_series")
if naming_series and naming_series.options:
prefixes = sorted(naming_series.options.split("\n"),
key=functools.cmp_to_key(lambda a, b: len(b) - len(a)))
for prefix in prefixes:
if prefix:
last = frappe.db.sql("""select max(name) from `tab{0}`
where name like %s""".format(doctype), prefix + "%")
if last and last[0][0]:
last = cint(last[0][0].replace(prefix, ""))
else:
last = 0
frappe.db.sql("""update tabSeries set current = %s
where name=%s""", (last, prefix))
def delete_bins(company_name):
frappe.db.sql("""delete from tabBin where warehouse in
(select name from tabWarehouse where company=%s)""", company_name)
def delete_lead_addresses(company_name):
"""Delete addresses to which leads are linked"""
leads = frappe.get_all("Lead", filters={"company": company_name})
leads = [ "'%s'"%row.get("name") for row in leads ]
addresses = []
if leads:
addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
in ({leads})""".format(leads=",".join(leads)))
if addresses:
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
def delete_communications(doctype, company_name, company_fieldname):
reference_docs = frappe.get_all(doctype, filters={company_fieldname:company_name})
reference_doc_names = [r.name for r in reference_docs]
communications = frappe.get_all("Communication", filters={"reference_doctype":doctype,"reference_name":["in", reference_doc_names]})
communication_names = [c.name for c in communications]
frappe.delete_doc("Communication", communication_names, ignore_permissions=True)

View File

@ -86,15 +86,6 @@ class TestCompany(unittest.TestCase):
self.delete_mode_of_payment(template) self.delete_mode_of_payment(template)
frappe.delete_doc("Company", template) frappe.delete_doc("Company", template)
def test_delete_communication(self):
from erpnext.setup.doctype.company.delete_company_transactions import delete_communications
company = create_child_company()
lead = create_test_lead_in_company(company)
communication = create_company_communication("Lead", lead)
delete_communications("Lead", "Test Company", "company")
self.assertFalse(frappe.db.exists("Communcation", communication))
self.assertFalse(frappe.db.exists({"doctype":"Comunication Link", "link_name": communication}))
def delete_mode_of_payment(self, company): def delete_mode_of_payment(self, company):
frappe.db.sql(""" delete from `tabMode of Payment Account` frappe.db.sql(""" delete from `tabMode of Payment Account`
where company =%s """, (company)) where company =%s """, (company))

View File

@ -0,0 +1,68 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
import frappe
import unittest
class TestTransactionDeletionRecord(unittest.TestCase):
def setUp(self):
create_company('Dunder Mifflin Paper Co')
def tearDown(self):
frappe.db.rollback()
def test_doctypes_contain_company_field(self):
tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co')
for doctype in tdr.doctypes:
contains_company = False
doctype_fields = frappe.get_meta(doctype.doctype_name).as_dict()['fields']
for doctype_field in doctype_fields:
if doctype_field['fieldtype'] == 'Link' and doctype_field['options'] == 'Company':
contains_company = True
break
self.assertTrue(contains_company)
def test_no_of_docs_is_correct(self):
for i in range(5):
create_task('Dunder Mifflin Paper Co')
tdr = create_transaction_deletion_request('Dunder Mifflin Paper Co')
for doctype in tdr.doctypes:
if doctype.doctype_name == 'Task':
self.assertEqual(doctype.no_of_docs, 5)
def test_deletion_is_successful(self):
create_task('Dunder Mifflin Paper Co')
create_transaction_deletion_request('Dunder Mifflin Paper Co')
tasks_containing_company = frappe.get_all('Task',
filters = {
'company' : 'Dunder Mifflin Paper Co'
})
self.assertEqual(tasks_containing_company, [])
def create_company(company_name):
company = frappe.get_doc({
'doctype': 'Company',
'company_name': company_name,
'default_currency': 'INR'
})
company.insert(ignore_if_duplicate = True)
def create_transaction_deletion_request(company):
tdr = frappe.get_doc({
'doctype': 'Transaction Deletion Record',
'company': company
})
tdr.insert()
tdr.submit()
return tdr
def create_task(company):
task = frappe.get_doc({
'doctype': 'Task',
'company': company,
'subject': 'Delete'
})
task.insert()

View File

@ -0,0 +1,40 @@
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Transaction Deletion Record', {
onload: function(frm) {
if (frm.doc.docstatus == 0) {
let doctypes_to_be_ignored_array;
frappe.call({
method: 'erpnext.setup.doctype.transaction_deletion_record.transaction_deletion_record.get_doctypes_to_be_ignored',
callback: function(r) {
doctypes_to_be_ignored_array = r.message;
populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm);
frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false);
frm.refresh_field('doctypes_to_be_ignored');
}
});
}
frm.get_field('doctypes_to_be_ignored').grid.cannot_add_rows = true;
frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false);
frm.refresh_field('doctypes_to_be_ignored');
},
refresh: function(frm) {
frm.fields_dict['doctypes_to_be_ignored'].grid.set_column_disp('no_of_docs', false);
frm.refresh_field('doctypes_to_be_ignored');
}
});
function populate_doctypes_to_be_ignored(doctypes_to_be_ignored_array, frm) {
if (!(frm.doc.doctypes_to_be_ignored)) {
var i;
for (i = 0; i < doctypes_to_be_ignored_array.length; i++) {
frm.add_child('doctypes_to_be_ignored', {
doctype_name: doctypes_to_be_ignored_array[i]
});
}
}
}

View File

@ -0,0 +1,79 @@
{
"actions": [],
"autoname": "TDL.####",
"creation": "2021-04-06 20:17:18.404716",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"doctypes",
"doctypes_to_be_ignored",
"amended_from",
"status"
],
"fields": [
{
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "doctypes",
"fieldtype": "Table",
"label": "Summary",
"options": "Transaction Deletion Record Item",
"read_only": 1
},
{
"fieldname": "doctypes_to_be_ignored",
"fieldtype": "Table",
"label": "Excluded DocTypes",
"options": "Transaction Deletion Record Item"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Transaction Deletion Record",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"hidden": 1,
"label": "Status",
"options": "Draft\nCompleted"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-05-08 23:13:48.049879",
"modified_by": "Administrator",
"module": "Setup",
"name": "Transaction Deletion Record",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,147 @@
# -*- coding: utf-8 -*-
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from __future__ import unicode_literals
from frappe.utils import cint
import frappe
from frappe.model.document import Document
from frappe import _
from frappe.desk.notifications import clear_notifications
class TransactionDeletionRecord(Document):
def validate(self):
frappe.only_for('System Manager')
company_obj = frappe.get_doc('Company', self.company)
if frappe.session.user != company_obj.owner and frappe.session.user != 'Administrator':
frappe.throw(_('Transactions can only be deleted by the creator of the Company or the Administrator.'),
frappe.PermissionError)
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in self.doctypes_to_be_ignored:
if doctype.doctype_name not in doctypes_to_be_ignored_list:
frappe.throw(_("DocTypes should not be added manually to the 'Excluded DocTypes' table. You are only allowed to remove entries from it. "), title=_("Not Allowed"))
def before_submit(self):
if not self.doctypes_to_be_ignored:
self.populate_doctypes_to_be_ignored_table()
self.delete_bins()
self.delete_lead_addresses()
company_obj = frappe.get_doc('Company', self.company)
# reset company values
company_obj.total_monthly_sales = 0
company_obj.sales_monthly_history = None
company_obj.save()
# Clear notification counts
clear_notifications()
singles = frappe.get_all('DocType', filters = {'issingle': 1}, pluck = 'name')
tables = frappe.get_all('DocType', filters = {'istable': 1}, pluck = 'name')
doctypes_to_be_ignored_list = singles
for doctype in self.doctypes_to_be_ignored:
doctypes_to_be_ignored_list.append(doctype.doctype_name)
docfields = frappe.get_all('DocField',
filters = {
'fieldtype': 'Link',
'options': 'Company',
'parent': ['not in', doctypes_to_be_ignored_list]},
fields=['parent', 'fieldname'])
for docfield in docfields:
if docfield['parent'] != self.doctype:
no_of_docs = frappe.db.count(docfield['parent'], {
docfield['fieldname'] : self.company
})
if no_of_docs > 0:
self.delete_version_log(docfield['parent'], docfield['fieldname'])
self.delete_communications(docfield['parent'], docfield['fieldname'])
# populate DocTypes table
if docfield['parent'] not in tables:
self.append('doctypes', {
'doctype_name' : docfield['parent'],
'no_of_docs' : no_of_docs
})
# delete the docs linked with the specified company
frappe.db.delete(docfield['parent'], {
docfield['fieldname'] : self.company
})
naming_series = frappe.db.get_value('DocType', docfield['parent'], 'autoname')
if naming_series:
if '#' in naming_series:
self.update_naming_series(naming_series, docfield['parent'])
def populate_doctypes_to_be_ignored_table(self):
doctypes_to_be_ignored_list = get_doctypes_to_be_ignored()
for doctype in doctypes_to_be_ignored_list:
self.append('doctypes_to_be_ignored', {
'doctype_name' : doctype
})
def update_naming_series(self, naming_series, doctype_name):
if '.' in naming_series:
prefix, hashes = naming_series.rsplit('.', 1)
else:
prefix, hashes = naming_series.rsplit('{', 1)
last = frappe.db.sql("""select max(name) from `tab{0}`
where name like %s""".format(doctype_name), prefix + '%')
if last and last[0][0]:
last = cint(last[0][0].replace(prefix, ''))
else:
last = 0
frappe.db.sql("""update tabSeries set current = %s where name=%s""", (last, prefix))
def delete_version_log(self, doctype, company_fieldname):
frappe.db.sql("""delete from `tabVersion` where ref_doctype=%s and docname in
(select name from `tab{0}` where `{1}`=%s)""".format(doctype,
company_fieldname), (doctype, self.company))
def delete_communications(self, doctype, company_fieldname):
reference_docs = frappe.get_all(doctype, filters={company_fieldname:self.company})
reference_doc_names = [r.name for r in reference_docs]
communications = frappe.get_all('Communication', filters={'reference_doctype':doctype,'reference_name':['in', reference_doc_names]})
communication_names = [c.name for c in communications]
frappe.delete_doc('Communication', communication_names, ignore_permissions=True)
def delete_bins(self):
frappe.db.sql("""delete from tabBin where warehouse in
(select name from tabWarehouse where company=%s)""", self.company)
def delete_lead_addresses(self):
"""Delete addresses to which leads are linked"""
leads = frappe.get_all('Lead', filters={'company': self.company})
leads = ["'%s'" % row.get("name") for row in leads]
addresses = []
if leads:
addresses = frappe.db.sql_list("""select parent from `tabDynamic Link` where link_name
in ({leads})""".format(leads=",".join(leads)))
if addresses:
addresses = ["%s" % frappe.db.escape(addr) for addr in addresses]
frappe.db.sql("""delete from tabAddress where name in ({addresses}) and
name not in (select distinct dl1.parent from `tabDynamic Link` dl1
inner join `tabDynamic Link` dl2 on dl1.parent=dl2.parent
and dl1.link_doctype<>dl2.link_doctype)""".format(addresses=",".join(addresses)))
frappe.db.sql("""delete from `tabDynamic Link` where link_doctype='Lead'
and parenttype='Address' and link_name in ({leads})""".format(leads=",".join(leads)))
frappe.db.sql("""update tabCustomer set lead_name=NULL where lead_name in ({leads})""".format(leads=",".join(leads)))
@frappe.whitelist()
def get_doctypes_to_be_ignored():
doctypes_to_be_ignored_list = ['Account', 'Cost Center', 'Warehouse', 'Budget',
'Party Account', 'Employee', 'Sales Taxes and Charges Template',
'Purchase Taxes and Charges Template', 'POS Profile', 'BOM',
'Company', 'Bank Account', 'Item Tax Template', 'Mode of Payment',
'Item Default', 'Customer', 'Supplier', 'GST Account']
return doctypes_to_be_ignored_list

View File

@ -0,0 +1,12 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
frappe.listview_settings['Transaction Deletion Record'] = {
get_indicator: function(doc) {
if (doc.docstatus == 0) {
return [__("Draft"), "red"];
} else {
return [__("Completed"), "green"];
}
}
};

View File

@ -0,0 +1,39 @@
{
"actions": [],
"creation": "2021-04-07 07:34:00.124124",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"doctype_name",
"no_of_docs"
],
"fields": [
{
"fieldname": "doctype_name",
"fieldtype": "Link",
"in_list_view": 1,
"label": "DocType",
"options": "DocType",
"reqd": 1
},
{
"fieldname": "no_of_docs",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Number of Docs"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-05-08 23:10:46.166744",
"modified_by": "Administrator",
"module": "Setup",
"name": "Transaction Deletion Record Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

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

View File

@ -732,7 +732,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype, "doctype": target_doctype,
"postprocess": update_details, "postprocess": update_details,
"field_no_map": [ "field_no_map": [
"taxes_and_charges" "taxes_and_charges",
"set_warehouse"
] ]
}, },
doctype +" Item": { doctype +" Item": {

View File

@ -243,16 +243,23 @@ class PurchaseReceipt(BuyingController):
def get_gl_entries(self, warehouse_account=None): def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map from erpnext.accounts.general_ledger import process_gl_map
gl_entries = []
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
self.make_tax_gl_entries(gl_entries)
self.get_asset_gl_entry(gl_entries)
return process_gl_map(gl_entries)
def make_item_gl_entries(self, gl_entries, warehouse_account=None):
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name) landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation") expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
gl_entries = []
warehouse_with_no_account = [] warehouse_with_no_account = []
negative_expense_to_be_booked = 0.0
stock_items = self.get_stock_items() stock_items = self.get_stock_items()
for d in self.get("items"): for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty): if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse): if warehouse_account.get(d.warehouse):
@ -263,21 +270,22 @@ class PurchaseReceipt(BuyingController):
if not stock_value_diff: if not stock_value_diff:
continue continue
warehouse_account_name = warehouse_account[d.warehouse]["account"]
warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency")
remarks = self.get("remarks") or _("Accounting Entry for Stock")
# If PR is sub-contracted and fg item rate is zero # If PR is sub-contracted and fg item rate is zero
# in that case if account for shource and target warehouse are same, # in that case if account for source and target warehouse are same,
# then GL entries should not be posted # then GL entries should not be posted
if flt(stock_value_diff) == flt(d.rm_supp_cost) \ if flt(stock_value_diff) == flt(d.rm_supp_cost) \
and warehouse_account.get(self.supplier_warehouse) \ and warehouse_account.get(self.supplier_warehouse) \
and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]: and warehouse_account_name == supplier_warehouse_account:
continue continue
gl_entries.append(self.get_gl_dict({ self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
"account": warehouse_account[d.warehouse]["account"], stock_rbnb, account_currency=warehouse_account_currency, item=d)
"against": stock_rbnb,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": stock_value_diff
}, warehouse_account[d.warehouse]["account_currency"], item=d))
# GL Entry for from warehouse or Stock Received but not billed # GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation # Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@ -287,43 +295,28 @@ class PurchaseReceipt(BuyingController):
credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \ credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \
if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount")) if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
if credit_amount: if credit_amount:
gl_entries.append(self.get_gl_dict({ account = warehouse_account[d.from_warehouse]['account'] \
"account": warehouse_account[d.from_warehouse]['account'] \ if d.from_warehouse else stock_rbnb
if d.from_warehouse else stock_rbnb,
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")),
"debit_in_account_currency": -1 * credit_amount
}, credit_currency, item=d))
negative_expense_to_be_booked += flt(d.item_tax_amount) self.add_gl_entry(gl_entries, account, d.cost_center,
-1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name,
debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d)
# Amount added through landed-cost-voucher # Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries: if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
account_currency = get_account_currency(account) account_currency = get_account_currency(account)
gl_entries.append(self.get_gl_dict({ credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or
"account": account, account_currency!=self.company_currency) else flt(amount["amount"]))
"account_currency": account_currency,
"against": warehouse_account[d.warehouse]["account"], self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, credit_amount, remarks,
"cost_center": d.cost_center, warehouse_account_name, credit_in_account_currency=flt(amount["amount"]),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), account_currency=account_currency, project=d.project, item=d)
"credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
account_currency!=self.company_currency) else flt(amount["amount"])),
"credit_in_account_currency": flt(amount["amount"]),
"project": d.project
}, item=d))
# sub-contracting warehouse # sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse): if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
gl_entries.append(self.get_gl_dict({ self.add_gl_entry(gl_entries, supplier_warehouse_account, d.cost_center, 0.0, flt(d.rm_supp_cost),
"account": warehouse_account[self.supplier_warehouse]["account"], remarks, warehouse_account_name, account_currency=supplier_warehouse_account_currency, item=d)
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(d.rm_supp_cost)
}, warehouse_account[self.supplier_warehouse]["account_currency"], item=d))
# divisional loss adjustment # divisional loss adjustment
valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \ valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \
@ -340,46 +333,32 @@ class PurchaseReceipt(BuyingController):
cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center") cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(self.get_gl_dict({ self.add_gl_entry(gl_entries, loss_account, cost_center, divisional_loss, 0.0, remarks,
"account": loss_account, warehouse_account_name, account_currency=credit_currency, project=d.project, item=d)
"against": warehouse_account[d.warehouse]["account"],
"cost_center": cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": divisional_loss,
"project": d.project
}, credit_currency, item=d))
elif d.warehouse not in warehouse_with_no_account or \ elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account: d.rejected_warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(d.warehouse) warehouse_with_no_account.append(d.warehouse)
elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items: elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
credit_currency = get_account_currency(service_received_but_not_billed_account) credit_currency = get_account_currency(service_received_but_not_billed_account)
gl_entries.append(self.get_gl_dict({
"account": service_received_but_not_billed_account,
"against": d.expense_account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Service"),
"project": d.project,
"credit": d.amount,
"voucher_detail_no": d.name
}, credit_currency, item=d))
debit_currency = get_account_currency(d.expense_account) debit_currency = get_account_currency(d.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service")
gl_entries.append(self.get_gl_dict({ self.add_gl_entry(gl_entries, service_received_but_not_billed_account, d.cost_center, 0.0, d.amount,
"account": d.expense_account, remarks, d.expense_account, account_currency=credit_currency, project=d.project,
"against": service_received_but_not_billed_account, voucher_detail_no=d.name, item=d)
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Service"),
"project": d.project,
"debit": d.amount,
"voucher_detail_no": d.name
}, debit_currency, item=d))
self.get_asset_gl_entry(gl_entries) self.add_gl_entry(gl_entries, d.expense_account, d.cost_center, d.amount, 0.0, remarks, service_received_but_not_billed_account,
account_currency = debit_currency, project=d.project, voucher_detail_no=d.name, item=d)
if warehouse_with_no_account:
frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
"\n".join(warehouse_with_no_account))
def make_tax_gl_entries(self, gl_entries):
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')])
# Cost center-wise amount breakup for other charges included for valuation # Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {} valuation_tax = {}
for tax in self.get("taxes"): for tax in self.get("taxes"):
@ -420,23 +399,33 @@ class PurchaseReceipt(BuyingController):
applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount)
amount_including_divisional_loss -= applicable_amount amount_including_divisional_loss -= applicable_amount
gl_entries.append( self.add_gl_entry(gl_entries, account, tax.cost_center, 0.0, applicable_amount, self.remarks or _("Accounting Entry for Stock"),
self.get_gl_dict({ against_account, item=tax)
"account": account,
"cost_center": tax.cost_center,
"credit": applicable_amount,
"remarks": self.remarks or _("Accounting Entry for Stock"),
"against": against_account
}, item=tax)
)
i += 1 i += 1
if warehouse_with_no_account: def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" + debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None,
"\n".join(warehouse_with_no_account)) project=None, voucher_detail_no=None, item=None):
gl_entry = {
"account": account,
"cost_center": cost_center,
"debit": debit,
"credit": credit,
"against_account": against_account,
"remarks": remarks,
}
return process_gl_map(gl_entries) if voucher_detail_no:
gl_entry.update({"voucher_detail_no": voucher_detail_no})
if debit_in_account_currency:
gl_entry.update({"debit_in_account_currency": debit_in_account_currency})
if credit_in_account_currency:
gl_entry.update({"credit_in_account_currency": credit_in_account_currency})
gl_entries.append(self.get_gl_dict(gl_entry, item=item))
def get_asset_gl_entry(self, gl_entries): def get_asset_gl_entry(self, gl_entries):
for item in self.get("items"): for item in self.get("items"):
@ -458,30 +447,21 @@ class PurchaseReceipt(BuyingController):
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
remarks = self.get("remarks") or _("Accounting Entry for Asset")
cwip_account_currency = get_account_currency(cwip_account) cwip_account_currency = get_account_currency(cwip_account)
# debit cwip account # debit cwip account
gl_entries.append(self.get_gl_dict({ debit_in_account_currency = (base_asset_amount
"account": cwip_account, if cwip_account_currency == self.company_currency else asset_amount)
"against": arbnb_account, self.add_gl_entry(gl_entries, cwip_account, item.cost_center, base_asset_amount, 0.0, remarks,
"cost_center": item.cost_center, arbnb_account, debit_in_account_currency=debit_in_account_currency, item=item)
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount,
"debit_in_account_currency": (base_asset_amount
if cwip_account_currency == self.company_currency else asset_amount)
}, item=item))
asset_rbnb_currency = get_account_currency(arbnb_account) asset_rbnb_currency = get_account_currency(arbnb_account)
# credit arbnb account # credit arbnb account
gl_entries.append(self.get_gl_dict({ credit_in_account_currency = (base_asset_amount
"account": arbnb_account, if asset_rbnb_currency == self.company_currency else asset_amount)
"against": cwip_account, self.add_gl_entry(gl_entries, arbnb_account, item.cost_center, 0.0, base_asset_amount, remarks,
"cost_center": item.cost_center, cwip_account, credit_in_account_currency=credit_in_account_currency, item=item)
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"credit": base_asset_amount,
"credit_in_account_currency": (base_asset_amount
if asset_rbnb_currency == self.company_currency else asset_amount)
}, item=item))
def add_lcv_gl_entries(self, item, gl_entries): def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@ -492,23 +472,13 @@ class PurchaseReceipt(BuyingController):
# This returns company's default cwip account # This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company) asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
gl_entries.append(self.get_gl_dict({ remarks = self.get("remarks") or _("Accounting Entry for Stock")
"account": expenses_included_in_asset_valuation,
"against": asset_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
gl_entries.append(self.get_gl_dict({ self.add_gl_entry(gl_entries, expenses_included_in_asset_valuation, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
"account": asset_account, remarks, asset_account, project=item.project, item=item)
"against": expenses_included_in_asset_valuation,
"cost_center": item.cost_center, self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
"remarks": self.get("remarks") or _("Accounting Entry for Stock"), remarks, expenses_included_in_asset_valuation, project=item.project, item=item)
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
def update_assets(self, item, valuation_rate): def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all('Asset', assets = frappe.db.get_all('Asset',

View File

@ -27,10 +27,11 @@ class TestQualityInspection(unittest.TestCase):
dn.reload() dn.reload()
self.assertRaises(QualityInspectionRejectedError, dn.submit) self.assertRaises(QualityInspectionRejectedError, dn.submit)
frappe.db.set_value("Quality Inspection Reading", {"parent": qa.name}, "status", "Accepted") frappe.db.set_value("Quality Inspection", qa.name, "status", "Accepted")
dn.reload() dn.reload()
dn.submit() dn.submit()
qa.reload()
qa.cancel() qa.cancel()
dn.reload() dn.reload()
dn.cancel() dn.cancel()

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, get_link_to_form, add_to_date, today from frappe.utils import cint, get_link_to_form, add_to_date, now, today
from erpnext.stock.stock_ledger import repost_future_sle from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role from frappe.utils.user import get_users_with_role
@ -127,9 +127,9 @@ def repost_entries():
check_if_stock_and_account_balance_synced(today(), d.name) check_if_stock_and_account_balance_synced(today(), d.name)
def get_repost_item_valuation_entries(): def get_repost_item_valuation_entries():
date = add_to_date(today(), hours=-3) date = add_to_date(now(), hours=-3)
return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation` return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
WHERE status != 'Completed' and creation <= %s and docstatus = 1 WHERE status != 'Completed' and creation <= %s and docstatus = 1
ORDER BY timestamp(posting_date, posting_time) asc, creation asc ORDER BY timestamp(posting_date, posting_time) asc, creation asc
""", date, as_dict=1) """, date, as_dict=1)

View File

@ -107,6 +107,7 @@ frappe.ui.form.on('Stock Entry', {
frappe.flags.hide_serial_batch_dialog = true; frappe.flags.hide_serial_batch_dialog = true;
} }
}); });
attach_bom_items(frm.doc.bom_no);
}, },
setup_quality_inspection: function(frm) { setup_quality_inspection: function(frm) {
@ -311,6 +312,7 @@ frappe.ui.form.on('Stock Entry', {
} }
frm.trigger("setup_quality_inspection"); frm.trigger("setup_quality_inspection");
attach_bom_items(frm.doc.bom_no)
}, },
stock_entry_type: function(frm){ stock_entry_type: function(frm){
@ -919,6 +921,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
method: "get_items", method: "get_items",
callback: function(r) { callback: function(r) {
if(!r.exc) refresh_field("items"); if(!r.exc) refresh_field("items");
if(me.frm.doc.bom_no) attach_bom_items(me.frm.doc.bom_no)
} }
}); });
} }
@ -1064,4 +1067,22 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
} }
function attach_bom_items(bom_no) {
if (check_should_not_attach_bom_items(bom_no)) return
frappe.db.get_doc("BOM",bom_no).then(bom => {
const {name, items} = bom
erpnext.stock.bom = {name, items:{}}
items.forEach(item => {
erpnext.stock.bom.items[item.item_code] = item;
});
});
}
function check_should_not_attach_bom_items(bom_no) {
return (
bom_no === undefined ||
(erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
);
}
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm})); $.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));

View File

@ -3,8 +3,9 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe.utils import cint, nowdate from frappe.utils import cint, flt
from frappe import throw, _ from frappe import throw, _
from collections import defaultdict
from frappe.utils.nestedset import NestedSet from frappe.utils.nestedset import NestedSet
from erpnext.stock import get_warehouse_account from erpnext.stock import get_warehouse_account
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import load_address_and_contact
@ -139,8 +140,6 @@ class Warehouse(NestedSet):
@frappe.whitelist() @frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False): def get_children(doctype, parent=None, company=None, is_root=False):
from erpnext.stock.utils import get_stock_value_from_bin
if is_root: if is_root:
parent = "" parent = ""
@ -153,13 +152,48 @@ def get_children(doctype, parent=None, company=None, is_root=False):
warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name') warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name')
company_currency = ''
if company:
company_currency = frappe.get_cached_value('Company', company, 'default_currency')
warehouse_wise_value = get_warehouse_wise_stock_value(company)
# return warehouses # return warehouses
for wh in warehouses: for wh in warehouses:
wh["balance"] = get_stock_value_from_bin(warehouse=wh.value) wh["balance"] = warehouse_wise_value.get(wh.value)
if company: if company_currency:
wh["company_currency"] = frappe.db.get_value('Company', company, 'default_currency') wh["company_currency"] = company_currency
return warehouses return warehouses
def get_warehouse_wise_stock_value(company):
warehouses = frappe.get_all('Warehouse',
fields = ['name', 'parent_warehouse'], filters = {'company': company})
parent_warehouse = {d.name : d.parent_warehouse for d in warehouses}
filters = {'warehouse': ('in', [data.name for data in warehouses])}
bin_data = frappe.get_all('Bin', fields = ['sum(stock_value) as stock_value', 'warehouse'],
filters = filters, group_by = 'warehouse')
warehouse_wise_stock_value = defaultdict(float)
for row in bin_data:
if not row.stock_value:
continue
warehouse_wise_stock_value[row.warehouse] = row.stock_value
update_value_in_parent_warehouse(warehouse_wise_stock_value,
parent_warehouse, row.warehouse, row.stock_value)
return warehouse_wise_stock_value
def update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value):
parent_warehouse = parent_warehouse_dict.get(warehouse)
if not parent_warehouse:
return
warehouse_wise_stock_value[parent_warehouse] += flt(stock_value)
update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict,
parent_warehouse, stock_value)
@frappe.whitelist() @frappe.whitelist()
def add_node(): def add_node():
from frappe.desk.treeview import make_tree_args from frappe.desk.treeview import make_tree_args

View File

@ -20,7 +20,7 @@ frappe.treeview_settings['Warehouse'] = {
onrender: function(node) { onrender: function(node) {
if (node.data && node.data.balance!==undefined) { if (node.data && node.data.balance!==undefined) {
$('<span class="balance-area pull-right">' $('<span class="balance-area pull-right">'
+ format_currency(Math.abs(node.data.balance), node.data.company_currency) + format_currency((node.data.balance), node.data.company_currency)
+ '</span>').insertBefore(node.$ul); + '</span>').insertBefore(node.$ul);
} }
} }