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

@ -30,3 +30,4 @@ ignore =
W191,
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
# ruleid: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
# ruleid: frappe-modifying-after-submit
self.status = 'Submitted'
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if flt(self.per_billed) < 100:
self.update_billing_status()
else:
# todook: frappe-modifying-after-submit
self.status = "Completed"
self.db_set("status", "Completed")
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted'
self.db_set('status', 'Submitted')
class TestDoc(Document):
pass
# ok: frappe-modifying-but-not-comitting
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
for item in self.child_table:
if item.value < 0:
self.remove(item)
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
x = "y"
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.
rules:
- id: frappe-modifying-after-submit
- id: frappe-modifying-but-not-comitting
patterns:
- pattern: self.$ATTR = ...
- pattern-inside: |
def on_submit(self, ...):
- pattern: |
def $METHOD(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: '$ATTR'
# 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: |
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]
severity: ERROR
- id: frappe-modifying-after-cancel
- id: frappe-modifying-but-not-comitting-other-method
patterns:
- pattern: self.$ATTR = ...
- pattern-inside: |
def on_cancel(self, ...):
- pattern: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
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: '$ATTR'
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
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]
severity: ERROR

View File

@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'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
pattern-either:
- pattern: _(...) + ... + _(...)
- pattern: _(...) + _(...)
- pattern: _("..." + "...")
- pattern-regex: '_\([^\)]*\\\s*'
- pattern-regex: '_\([^\)]*\\\s*' # lines broken by `\`
- pattern-regex: '_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
@ -53,8 +54,8 @@ rules:
- id: frappe-translation-js-splitting
pattern-either:
- pattern-regex: '__\([^\)]*[\+\\]\s*'
- pattern: __('...' + '...')
- pattern-regex: '__\([^\)]*[\\]\s+'
- pattern: __('...' + '...', ...)
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.

View File

@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
- version-13-hotfix
- version-13-pre-release
jobs:
semgrep:
name: Frappe Linter
@ -14,11 +16,19 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.8
- name: Run semgrep
- name: Setup semgrep
run: |
python -m pip install -q semgrep
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)
[[ -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
- 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

View File

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

View File

@ -146,7 +146,7 @@
},
{
"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",
"fieldtype": "Data",
"label": "Import from Google Sheets"
@ -202,7 +202,7 @@
],
"hide_toolbar": 1,
"links": [],
"modified": "2021-02-10 19:29:59.027325",
"modified": "2021-05-12 14:17:37.777246",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",

View File

@ -47,6 +47,13 @@ class BankStatementImport(DataImport):
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.utils.scheduler import is_scheduler_inactive
@ -67,6 +74,7 @@ class BankStatementImport(DataImport):
data_import=self.name,
bank_account=self.bank_account,
import_file_path=self.import_file,
google_sheets_url=self.google_sheets_url,
bank=self.bank,
template_options=self.template_options,
now=frappe.conf.developer_mode or frappe.flags.in_test,
@ -90,16 +98,18 @@ def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
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"""
update_mapping_db(bank, template_options)
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
if import_file_path:
add_bank_account(data, bank_account)
write_files(import_file, data)

View File

@ -1,196 +1,82 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2018-01-02 15:48:58.768352",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company",
"cgst_account",
"sgst_account",
"igst_account",
"cess_account",
"is_reverse_charge_account"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 1,
"fieldname": "company",
"fieldtype": "Link",
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 2,
"fieldname": "cgst_account",
"fieldtype": "Link",
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 2,
"fieldname": "sgst_account",
"fieldtype": "Link",
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 2,
"fieldname": "igst_account",
"fieldtype": "Link",
"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
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"columns": 2,
"fieldname": "cess_account",
"fieldtype": "Link",
"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": "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
"options": "Account"
},
{
"columns": 1,
"default": "0",
"fieldname": "is_reverse_charge_account",
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Reverse Charge Account"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"index_web_pages_for_search": 1,
"istable": 1,
"max_attachments": 0,
"modified": "2018-01-02 15:52:22.335988",
"links": [],
"modified": "2021-04-09 12:30:25.889993",
"modified_by": "Administrator",
"module": "Accounts",
"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
"track_changes": 1
}

View File

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

View File

@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this;
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) {
// show debit_to in print format
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 = {
'Delivery Note': 'Delivery',
'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) {
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() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
@ -821,15 +843,6 @@ frappe.ui.form.on('Sales Invoice', {
"fieldtype": "Date",
"reqd": 1,
},
{
"label" : __("Currency"),
"fieldname": "currency",
"fieldtype": "Link",
"options": "Currency",
"default": frm.doc.currency,
"reqd": 1,
"read_only": 1
},
{
fieldtype: 'Column Break',
fieldname: 'col_break_1',
@ -845,9 +858,7 @@ frappe.ui.form.on('Sales Invoice', {
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"default": frm.doc.project,
"reqd": 1,
"read_only": 1
"default": frm.doc.project
},
],
primary_action: function() {
@ -857,24 +868,33 @@ frappe.ui.form.on('Sales Invoice', {
args: {
from_time: data.from_time,
to_time: data.to_time,
project: data.project,
currency: data.currency
project: data.project
},
callback: function(r) {
if(!r.exc) {
if(r.message.length > 0) {
frm.clear_table('timesheets')
r.message.forEach((d) => {
frm.add_child('timesheets',{
'activity_type': d.activity_type,
'description': d.description,
'time_sheet': d.parent,
'billing_hours': d.billing_hours,
'billing_amount': d.billing_amount,
'timesheet_detail': d.name
let exchange_rate = 1.0;
if (frm.doc.currency != d.currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: d.currency,
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 {
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")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);

View File

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

View File

@ -22,6 +22,9 @@ def get_party_details(inv):
party_type = 'Supplier'
party = inv.supplier
if not party:
frappe.throw(_("Please select {0} first").format(party_type))
return party_type, party
def get_party_tax_withholding_details(inv, tax_withholding_category=None):

View File

@ -135,7 +135,7 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit
# from consolidated financial statement
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:
key = period if consolidated else period.key

View File

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

View File

@ -195,8 +195,7 @@ class Asset(AccountsController):
# If depreciation is already completed (for double declining balance)
if skip_row: continue
depreciation_amount = self.get_depreciation_amount(value_after_depreciation,
d.total_number_of_depreciations, d)
depreciation_amount = get_depreciation_amount(self, value_after_depreciation, d)
if not has_pro_rata or n < cint(number_of_pending_depreciations) - 1:
schedule_date = add_months(d.depreciation_start_date,
@ -208,7 +207,7 @@ class Asset(AccountsController):
# For first row
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)
# 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,
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)
monthly_schedule_date = add_months(schedule_date, 1)
@ -365,24 +364,6 @@ class Asset(AccountsController):
def get_value_after_depreciation(self, idx):
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):
for row in self.get('finance_books'):
accumulated_depreciation_after_full_schedule = [d.accumulated_depreciation_amount
@ -575,6 +556,13 @@ class Asset(AccountsController):
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():
assets = frappe.get_all(
"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):
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):
period_start_date = add_months(date,
cint(frequency) * -1)
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.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():
if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category()

View File

@ -62,6 +62,7 @@ class RequestforQuotation(BuyingController):
for supplier in self.suppliers:
supplier.email_sent = 0
supplier.quote_status = 'Pending'
self.send_to_supplier()
def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled')
@ -81,7 +82,7 @@ class RequestforQuotation(BuyingController):
def send_to_supplier(self):
"""Sends RFQ mail to involved 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)
# 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'):
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"):
self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret)

View File

@ -100,6 +100,10 @@ status_map = {
["Queued", "eval:self.status == 'Queued'"],
["Failed", "eval:self.status == 'Failed'"],
["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)
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_failed:
if qa_doc.status != 'Accepted':
frappe.throw(_("Row {0}: Quality Inspection rejected for item {1}")
.format(d.idx, d.item_code), QualityInspectionRejectedError)
elif qa_required :

View File

@ -1,6 +1,7 @@
from __future__ import unicode_literals
import frappe, base64, hashlib, hmac, json
from frappe.utils import cstr
from frappe import _
def verify_request():
@ -146,20 +147,17 @@ def rename_address(address, customer):
def link_items(items_list, woocommerce_settings, sys_lang):
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}):
#Edit Item
item = frappe.get_doc("Item", {"woocommerce_id": item_woo_com_id})
else:
if not frappe.db.get_value("Item", {"woocommerce_id": item_woo_com_id}, 'name'):
#Create 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_code = _("woocommerce - {0}", sys_lang).format(item_data.get("product_id"))
item.woocommerce_id = item_data.get("product_id")
item.item_group = _("WooCommerce Products", sys_lang)
item.stock_uom = woocommerce_settings.uom or _("Nos", sys_lang)
item.woocommerce_id = item_woo_com_id
item.flags.ignore_mandatory = True
item.save()
@ -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"):
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")
new_sales_order.append("items", {
"item_code": found_item.item_code,
"item_code": found_item.name,
"item_name": found_item.item_name,
"description": found_item.item_name,
"delivery_date": new_sales_order.delivery_date,

View File

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

View File

@ -32,10 +32,12 @@ def create_customer(shopify_customer, shopify_settings):
raise e
def create_customer_address(customer, shopify_customer):
if not shopify_customer.get("addresses"):
return
addresses = shopify_customer.get("addresses", [])
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)
try :
frappe.get_doc({

View File

@ -268,9 +268,11 @@ doc_events = {
},
"Purchase Invoice": {
"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.validate_returns"
"erpnext.regional.united_arab_emirates.utils.validate_returns",
"erpnext.regional.india.utils.update_taxable_values"
]
},
"Payment Entry": {
@ -304,8 +306,7 @@ doc_events = {
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
auto_cancel_exempted_doctypes= [
"Payment Entry",
"Inpatient Medication Entry",
"Timesheet"
"Inpatient Medication Entry"
]
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.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_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment",
"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_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"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.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.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': {
'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': [
{
'label': _('Leave and Attendance'),
'items': ['Attendance', 'Attendance Request', 'Leave Application', 'Leave Allocation', 'Employee Checkin']
'label': _('Attendance'),
'items': ['Attendance', 'Attendance Request', 'Employee Checkin']
},
{
'label': _('Leave'),
'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment']
},
{
'label': _('Lifecycle'),
@ -30,10 +34,6 @@ def get_data():
'label': _('Benefit'),
'items': ['Employee Benefit Application', 'Employee Benefit Claim']
},
{
'label': _('Evaluation'),
'items': ['Appraisal']
},
{
'label': _('Payroll'),
'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account']
@ -42,5 +42,9 @@ def get_data():
'label': _('Training'),
'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",
"auto_leave_encashment",
"restrict_backdated_leave_application",
"automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings",
"check_vacancies"
],
@ -133,12 +132,6 @@
"label": "Role Allowed to Create Backdated Leave Application",
"options": "Role"
},
{
"default": "0",
"fieldname": "automatically_allocate_leaves_based_on_leave_policy",
"fieldtype": "Check",
"label": "Automatically Allocate Leaves Based On Leave Policy"
},
{
"default": "1",
"fieldname": "send_leave_notification",
@ -155,7 +148,7 @@
"idx": 1,
"issingle": 1,
"links": [],
"modified": "2021-04-26 10:52:56.192773",
"modified": "2021-05-11 10:52:56.192773",
"modified_by": "Administrator",
"module": "HR",
"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))
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
from erpnext.hr.utils import allocate_earned_leaves
i = 0
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,
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):
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)

View File

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

View File

@ -4,35 +4,22 @@
frappe.ui.form.on('Leave Policy Assignment', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
},
refresh: function(frm) {
if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
frm.add_custom_button(__("Grant Leave"), function() {
frappe.call({
doc: frm.doc,
method: "grant_leave_alloc_for_employee",
callback: function(r) {
let leave_allocations = r.message;
let msg = frm.events.get_success_message(leave_allocations);
frappe.msgprint(msg);
cur_frm.refresh();
frm.set_query('leave_policy', function() {
return {
filters: {
"docstatus": 1
}
};
});
frm.set_query('leave_period', function() {
return {
filters: {
"is_active": 1,
"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) {

View File

@ -17,6 +17,9 @@ class LeavePolicyAssignment(Document):
self.validate_policy_assignment_overlap()
self.set_dates()
def on_submit(self):
self.grant_leave_alloc_for_employee()
def set_dates(self):
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"])
@ -75,7 +78,7 @@ class LeavePolicyAssignment(Document):
from_date=self.effective_from,
to_date=self.effective_to,
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 = self.leave_policy,
carry_forward=carry_forward
@ -131,22 +134,6 @@ class LeavePolicyAssignment(Document):
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()
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.leave_period = data.leave_period or None
assignment.carry_forward = data.carry_forward
assignment.save()
try:
assignment.submit()
except frappe.exceptions.ValidationError:
continue
frappe.db.commit()
docs_name.append(assignment.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():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
@ -197,4 +173,3 @@ def get_leave_type_details():
for d in leave_types:
leave_type_details.setdefault(d.name, d)
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",
target: cur_list,
setters: {
employee_name: '',
company: '',
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 () {

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_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()
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_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()

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
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):
# if approver does not have permissions, share
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.add_gst_category_in_delivery_note
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.v12_0.add_document_type_field_for_italy_einvoicing
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")
def validate_project(self, data):
if self.parent_project != data.project:
frappe.throw(_("Row {0}: Poject must be same as {1}.")).format(data.idx, self.parent_project)
if self.parent_project and self.parent_project != data.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):
if not value or ignore_validation:
@ -208,27 +208,25 @@ class Timesheet(Document):
ts_detail.billing_rate = 0.0
@frappe.whitelist()
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None, currency=None):
condition = field = join = ''
def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None):
condition = ''
if project:
condition += "and tsd.project = %(project)s"
if parent:
condition = "AND tsd.parent = %(parent)s"
condition += "AND tsd.parent = %(parent)s"
if from_time and to_time:
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,
tsd.parent as parent, tsd.billing_hours as billing_hours,
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
{1} where tsd.parenttype = 'Timesheet'
and tsd.docstatus=1
and tsd.project = %(project)s {2}
INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent
WHERE tsd.parenttype = 'Timesheet'
and tsd.docstatus=1 {0}
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.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);
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(["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(["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(["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.doc.party_account_currency, "advances");
}

View File

@ -74,9 +74,18 @@ erpnext.SerialNoBatchSelector = Class.extend({
fieldname: 'qty',
fieldtype:'Float',
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),
},
...get_pending_qty_fields(me),
{
fieldname: 'uom',
read_only: 1,
fieldtype: 'Link',
options: 'UOM',
label: __('UOM'),
default: me.item.uom
},
{
fieldname: 'auto_fetch_button',
fieldtype:'Button',
@ -173,6 +182,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
if (this.has_batch && !this.has_serial_no) {
this.update_total_qty();
this.update_pending_qtys();
}
this.dialog.show();
@ -313,7 +323,21 @@ erpnext.SerialNoBatchSelector = Class.extend({
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() {
var me = this;
@ -415,6 +439,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
}
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;
border-radius: var(--border-radius-md);
box-shadow: var(--shadow-base);
position: relative;
&:hover {
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 {
display: flex;
align-items: center;
@ -766,9 +775,10 @@
> .payment-modes {
display: flex;
padding-bottom: var(--padding-sm);
margin-bottom: var(--margin-xs);
margin-bottom: var(--margin-sm);
overflow-x: scroll;
overflow-y: hidden;
flex-shrink: 0;
> .payment-mode-wrapper {
min-width: 40%;
@ -825,9 +835,24 @@
> .fields-numpad-container {
display: flex;
flex: 1;
height: 100%;
position: relative;
justify-content: flex-end;
> .fields-section {
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 {
@ -835,6 +860,7 @@
display: flex;
justify-content: flex-end;
align-items: flex-end;
max-width: 50%;
.numpad-container {
display: grid;
@ -861,6 +887,7 @@
margin-bottom: var(--margin-sm);
justify-content: center;
flex-direction: column;
flex-shrink: 0;
> .totals {
display: flex;

View File

@ -172,7 +172,7 @@
</thead>
<tbody>
<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>

View File

@ -3,148 +3,21 @@
# For license information, please see license.txt
from __future__ import unicode_literals
import os
import json
import frappe
from six import iteritems
from frappe import _
from frappe.model.document import Document
import json
from six import iteritems
from frappe.utils import flt, getdate
from frappe.utils import flt, cstr
from erpnext.regional.india import state_numbers
class GSTR3BReport(Document):
def before_save(self):
def validate(self):
self.get_data()
def get_data(self):
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.report_dict = json.loads(get_json('gstr_3b_report_template'))
self.gst_details = self.get_company_gst_details()
self.report_dict["gstin"] = self.gst_details.get("gstin")
@ -152,23 +25,19 @@ class GSTR3BReport(Document):
self.month_no = get_period(self.month)
self.account_heads = self.get_account_heads()
outward_supply_tax_amounts = self.get_tax_amounts("Sales Invoice")
inward_supply_tax_amounts = self.get_tax_amounts("Purchase Invoice", reverse_charge="Y")
self.get_outward_supply_details("Sales Invoice")
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()
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)
inter_state_supplies = self.get_inter_state_supplies(self.gst_details.get("gst_state_number"))
self.get_itc_reversal_entries()
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.missing_field_invoices = self.get_missing_field_invoices()
self.json_output = frappe.as_json(self.report_dict)
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)
def set_itc_details(self, itc_details):
itc_type_map = {
itc_eligible_type_map = {
'IMPG': 'Import Of Capital Goods',
'IMPS': 'Import Of Service',
'ISRC': 'ITC on Reverse Charge',
'ISD': 'Input Service Distributor',
'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"]
for d in self.report_dict["itc_elg"]["itc_avl"]:
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)
itc_type = itc_eligible_type_map.get(d["ty"])
for key in ['iamt', 'camt', 'samt', 'csamt']:
d[key] = flt(itc_details.get(itc_type, {}).get(key))
net_itc[key] += flt(d[key], 2)
for account_head in self.account_heads:
itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1]
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2)
for d in self.report_dict["itc_elg"]["itc_inelg"]:
itc_type = itc_ineligible_map.get(d["ty"])
for key in ['iamt', 'camt', 'samt', 'csamt']:
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 = {
'sgst_account': 'samt',
'cess_account': 'csamt',
'cgst_account': 'camt',
'igst_account': 'iamt'
}
net_itc = self.report_dict["itc_elg"]["itc_net"]
txval = 0
total_taxable_value = self.get_total_taxable_value(doctype, reverse_charge)
for entry in reversal_entries:
if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules':
index = 0
else:
index = 1
for gst_category in gst_category_list:
txval += total_taxable_value.get(gst_category,0)
for account_head in self.account_heads:
for account_type, account_name in iteritems(account_head):
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"))))
for key in ['camt', 'samt', 'iamt', 'csamt']:
if entry.account in self.account_heads.get(key):
self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount)
net_itc[key] -= flt(entry.amount)
def get_itc_details(self):
itc_amount = frappe.db.sql("""
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
t.account_head, s.eligibility_for_itc, s.reverse_charge
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
where s.docstatus = 1 and t.parent = s.name
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, s.eligibility_for_itc
""",
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
itc_amounts = frappe.db.sql("""
SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax,
sum(itc_central_tax) as itc_central_tax,
sum(itc_state_tax) as itc_state_tax,
sum(itc_cess_amount) as itc_cess_amount
FROM `tabPurchase Invoice`
WHERE docstatus = 1
and is_opening = 'No'
and month(posting_date) = %s and year(posting_date) = %s and company = %s
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 = {}
for d in itc_amount:
itc_details.setdefault((d.gst_category, d.eligibility_for_itc, d.reverse_charge, d.account_head),{
"amount": d.tax_amount
for d in itc_amounts:
itc_details.setdefault(d.eligibility_for_itc, {
'iamt': d.itc_integrated_tax,
'camt': d.itc_central_tax,
'samt': d.itc_state_tax,
'csamt': d.itc_cess_amount
})
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):
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent
inward_nil_exempt = frappe.db.sql("""
SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
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 (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %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)
inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply
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)
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
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)
inward_nil_exempt_details = {
"gst": {
@ -388,37 +163,193 @@ class GSTR3BReport(Document):
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":
tax_template = 'Sales Taxes and Charges'
elif doctype == "Purchase Invoice":
tax_template = 'Purchase Taxes and Charges'
tax_amounts = frappe.db.sql("""
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
from `tab{doctype}` s , `tab{template}` t
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)
self.items_based_on_tax_rate = {}
self.invoice_cess = frappe._dict()
self.cgst_sgst_invoices = []
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:
tax_details.setdefault(
(d.account_head,d.gst_category),{
"amount": d.get("tax_amount"),
}
)
for parent, account, item_wise_tax_detail, tax_amount in tax_details:
if account in self.account_heads.get('csamt'):
self.invoice_cess.setdefault(parent, 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):
gst_details = frappe.get_all("Address",
fields=["gstin", "gst_state", "gst_state_number"],
filters={
@ -431,20 +362,28 @@ class GSTR3BReport(Document):
frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address))
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",
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"],
filters={
"company":self.company
})
account_heads = {}
gst_settings_accounts = frappe.get_all("GST Account",
filters={'company': self.company, 'is_reverse_charge_account': 0},
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
if not gst_settings_accounts:
frappe.throw(_("Please set GST Accounts in GST Settings"))
for d in gst_settings_accounts:
for acc, val in d.items():
account_heads.setdefault(account_map.get(acc), []).append(val)
if account_heads:
return account_heads
else:
frappe.throw(_("Please set account heads in GST Settings for Compnay {0}").format(self.company))
def get_missing_field_invoices(self):
missing_field_invoices = []
for doctype in ["Sales Invoice", "Purchase Invoice"]:
@ -456,26 +395,32 @@ class GSTR3BReport(Document):
party_type = 'Supplier'
party = 'supplier'
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
docnames = frappe.db.sql(
"""
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
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:
missing_field_invoices.append(d.name)
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)
return state_code
def get_period(month, year=None):
month_no = {
"January": 1,
"February": 2,
@ -499,13 +444,11 @@ def get_period(month, year=None):
@frappe.whitelist()
def view_report(name):
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
return json.loads(json_data)
@frappe.whitelist()
def make_json(name):
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
file_name = "GST3B.json"
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)
self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 36),
self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 54)
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
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):
# 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:
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('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True):
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 = [
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\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',
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',
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',
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',
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 = [
@ -236,6 +244,23 @@ def make_custom_fields(update=True):
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 = [
dict(fieldname='is_inter_state', label='Is Inter State',
fieldtype='Check', insert_after='disabled', print_hide=1),
@ -469,6 +494,7 @@ def make_custom_fields(update=True):
'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,
'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,
'Tax Category': inter_state_gst_field,
'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],
'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 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],
'Salary Component': [
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"):
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)
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"):
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)
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')
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):
basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"])
if not (basic_component and hra_component):
@ -697,10 +680,19 @@ def validate_state_code(state_code, address):
return int(state_code)
@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_settings_accounts = frappe.get_all("GST Account",
filters={"parent": "GST Settings", "company": company},
filters=filters,
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
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
def update_grand_total_for_rcm(doc, method):
def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
if not base_gst_tax:
return
base_gst_tax = 0
base_reverse_charge_booked = 0
if doc.reverse_charge == 'Y':
doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax
doc.base_total_taxes_and_charges -= base_gst_tax
gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1)
reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
update_totals(gst_tax, base_gst_tax, doc)
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_gst_accounts(doc.company, only_non_reverse_charge=1)
non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
for tax in doc.get('taxes'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if tax.account_head in non_reverse_charge_accounts:
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 flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
account_currency = get_account_currency(tax.account_head)
if base_gst_tax != base_reverse_charge_booked:
msg = _("Booked reverse charge is not equal to applied tax amount")
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(
{
"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)
)
frappe.throw(msg)
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):
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', [])
if country != 'India':
return
base_gst_tax = 0
gst_tax = 0
# Initialize values
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'):
if tax.category not in ("Total", "Valuation and Total"):
continue
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
base_gst_tax += tax.base_tax_amount_after_discount_amount
gst_tax += tax.tax_amount_after_discount_amount
return gst_tax, base_gst_tax
if tax.account_head in gst_accounts.get('igst_account', []):
doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('sgst_account', []):
doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('cgst_account', []):
doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount)
if tax.account_head in gst_accounts.get('cess_account', []):
doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount)
@frappe.whitelist()
def get_regional_round_off_accounts(company, account_list):
@ -879,3 +833,24 @@ def update_taxable_values(doc, method):
if total_charges != additional_taxes:
diff = additional_taxes - total_charges
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"),
"fieldtype": "Select",
"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"
}
],

View File

@ -32,6 +32,7 @@ class Gstr1Report(object):
reverse_charge,
return_against,
is_return,
is_debit_note,
gst_category,
export_type,
port_code,
@ -42,7 +43,7 @@ class Gstr1Report(object):
def run(self):
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()
if self.invoices:
@ -62,9 +63,9 @@ class Gstr1Report(object):
for rate, items in items_based_on_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("C" if invoice_details.return_against else "R")
row.append("C" if invoice_details.is_return else "D")
if taxable_value:
self.data.append(row)
@ -105,7 +106,7 @@ class Gstr1Report(object):
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
row = []
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))
elif fieldname == "invoice_value":
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":
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"):
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."))
if self.filters.get("type_of_business") == "B2C Large":
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))
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))
elif self.filters.get("type_of_business") == "B2C Small":
conditions += """ and (
conditions += """ AND (
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":
conditions += """ and is_return = 1 """
elif self.filters.get("type_of_business") == "CDNR-REG":
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":
conditions += """ and is_return !=1 and gst_category = 'Overseas' """
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
return conditions
def get_invoice_items(self):
@ -403,7 +404,7 @@ class Gstr1Report(object):
"width": 100
}
]
elif self.filters.get("type_of_business") == "CDNR":
elif self.filters.get("type_of_business") == "CDNR-REG":
self.invoice_columns = [
{
"fieldname": "customer_gstin",
@ -437,6 +438,17 @@ class Gstr1Report(object):
"options": "Sales Invoice",
"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",
"label": "Reason For Issuing document",
@ -449,6 +461,11 @@ class Gstr1Report(object):
"fieldtype": "Data",
"width": 120
},
{
"fieldname": "gst_category",
"label": "GST Category",
"fieldtype": "Data"
},
{
"fieldname": "invoice_value",
"label": "Invoice Value",
@ -589,6 +606,12 @@ def get_json(filters, report_name, data):
out = get_export_json(res)
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 {
'report_name': report_name,
@ -628,7 +651,6 @@ def get_b2b_json(res, gstin):
return out
def get_b2cs_json(data, gstin):
company_state_number = gstin[0:2]
out = []
@ -713,6 +735,54 @@ def get_export_json(res):
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):
return {
"inum": row["invoice_number"],

View File

@ -56,10 +56,6 @@ erpnext.PointOfSale.Controller = class {
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({
title: __('Create POS Opening Entry'),
static: true,
@ -105,6 +101,10 @@ erpnext.PointOfSale.Controller = class {
primary_action_label: __('Submit')
});
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) {

View File

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

View File

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

View File

@ -614,3 +614,12 @@ def get_default_company_address(name, sort_key='is_primary_address', existing_ad
return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0]
else:
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)
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):
frappe.db.sql(""" delete from `tabMode of Payment Account`
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,
"postprocess": update_details,
"field_no_map": [
"taxes_and_charges"
"taxes_and_charges",
"set_warehouse"
]
},
doctype +" Item": {

View File

@ -243,16 +243,23 @@ class PurchaseReceipt(BuyingController):
def get_gl_entries(self, warehouse_account=None):
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")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
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'))
gl_entries = []
warehouse_with_no_account = []
negative_expense_to_be_booked = 0.0
stock_items = self.get_stock_items()
for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse):
@ -263,21 +270,22 @@ class PurchaseReceipt(BuyingController):
if not stock_value_diff:
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
# 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
if flt(stock_value_diff) == flt(d.rm_supp_cost) \
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
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[d.warehouse]["account"],
"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))
self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# 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")) \
if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
if credit_amount:
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[d.from_warehouse]['account'] \
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))
account = warehouse_account[d.from_warehouse]['account'] \
if d.from_warehouse else stock_rbnb
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:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
account_currency = get_account_currency(account)
gl_entries.append(self.get_gl_dict({
"account": account,
"account_currency": account_currency,
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"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))
credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or
account_currency!=self.company_currency) else flt(amount["amount"]))
self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, credit_amount, remarks,
warehouse_account_name, credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency, project=d.project, item=d)
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[self.supplier_warehouse]["account"],
"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))
self.add_gl_entry(gl_entries, supplier_warehouse_account, d.cost_center, 0.0, flt(d.rm_supp_cost),
remarks, warehouse_account_name, account_currency=supplier_warehouse_account_currency, item=d)
# divisional loss adjustment
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")
gl_entries.append(self.get_gl_dict({
"account": loss_account,
"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))
self.add_gl_entry(gl_entries, loss_account, cost_center, divisional_loss, 0.0, remarks,
warehouse_account_name, account_currency=credit_currency, project=d.project, item=d)
elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account:
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:
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)
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)
remarks = self.get("remarks") or _("Accounting Entry for Service")
gl_entries.append(self.get_gl_dict({
"account": d.expense_account,
"against": service_received_but_not_billed_account,
"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.add_gl_entry(gl_entries, service_received_but_not_billed_account, d.cost_center, 0.0, d.amount,
remarks, d.expense_account, account_currency=credit_currency, project=d.project,
voucher_detail_no=d.name, 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
valuation_tax = {}
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)
amount_including_divisional_loss -= applicable_amount
gl_entries.append(
self.get_gl_dict({
"account": account,
"cost_center": tax.cost_center,
"credit": applicable_amount,
"remarks": self.remarks or _("Accounting Entry for Stock"),
"against": against_account
}, item=tax)
)
self.add_gl_entry(gl_entries, account, tax.cost_center, 0.0, applicable_amount, self.remarks or _("Accounting Entry for Stock"),
against_account, item=tax)
i += 1
if warehouse_with_no_account:
frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
"\n".join(warehouse_with_no_account))
def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None,
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):
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)
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)
# debit cwip account
gl_entries.append(self.get_gl_dict({
"account": cwip_account,
"against": arbnb_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount,
"debit_in_account_currency": (base_asset_amount
debit_in_account_currency = (base_asset_amount
if cwip_account_currency == self.company_currency else asset_amount)
}, item=item))
self.add_gl_entry(gl_entries, cwip_account, item.cost_center, base_asset_amount, 0.0, remarks,
arbnb_account, debit_in_account_currency=debit_in_account_currency, item=item)
asset_rbnb_currency = get_account_currency(arbnb_account)
# credit arbnb account
gl_entries.append(self.get_gl_dict({
"account": arbnb_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"credit": base_asset_amount,
"credit_in_account_currency": (base_asset_amount
credit_in_account_currency = (base_asset_amount
if asset_rbnb_currency == self.company_currency else asset_amount)
}, item=item))
self.add_gl_entry(gl_entries, arbnb_account, item.cost_center, 0.0, base_asset_amount, remarks,
cwip_account, credit_in_account_currency=credit_in_account_currency, item=item)
def add_lcv_gl_entries(self, item, gl_entries):
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
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
gl_entries.append(self.get_gl_dict({
"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))
remarks = self.get("remarks") or _("Accounting Entry for Stock")
gl_entries.append(self.get_gl_dict({
"account": asset_account,
"against": expenses_included_in_asset_valuation,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project
}, item=item))
self.add_gl_entry(gl_entries, expenses_included_in_asset_valuation, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
remarks, asset_account, project=item.project, item=item)
self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
remarks, expenses_included_in_asset_valuation, project=item.project, item=item)
def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all('Asset',

View File

@ -27,10 +27,11 @@ class TestQualityInspection(unittest.TestCase):
dn.reload()
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.submit()
qa.reload()
qa.cancel()
dn.reload()
dn.cancel()

View File

@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe, erpnext
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.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role
@ -127,7 +127,7 @@ def repost_entries():
check_if_stock_and_account_balance_synced(today(), d.name)
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`
WHERE status != 'Completed' and creation <= %s and docstatus = 1

View File

@ -107,6 +107,7 @@ frappe.ui.form.on('Stock Entry', {
frappe.flags.hide_serial_batch_dialog = true;
}
});
attach_bom_items(frm.doc.bom_no);
},
setup_quality_inspection: function(frm) {
@ -311,6 +312,7 @@ frappe.ui.form.on('Stock Entry', {
}
frm.trigger("setup_quality_inspection");
attach_bom_items(frm.doc.bom_no)
},
stock_entry_type: function(frm){
@ -919,6 +921,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
method: "get_items",
callback: function(r) {
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}));

View File

@ -3,8 +3,9 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe.utils import cint, nowdate
from frappe.utils import cint, flt
from frappe import throw, _
from collections import defaultdict
from frappe.utils.nestedset import NestedSet
from erpnext.stock import get_warehouse_account
from frappe.contacts.address_and_contact import load_address_and_contact
@ -139,8 +140,6 @@ class Warehouse(NestedSet):
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False):
from erpnext.stock.utils import get_stock_value_from_bin
if is_root:
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')
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
for wh in warehouses:
wh["balance"] = get_stock_value_from_bin(warehouse=wh.value)
if company:
wh["company_currency"] = frappe.db.get_value('Company', company, 'default_currency')
wh["balance"] = warehouse_wise_value.get(wh.value)
if company_currency:
wh["company_currency"] = company_currency
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()
def add_node():
from frappe.desk.treeview import make_tree_args

View File

@ -20,7 +20,7 @@ frappe.treeview_settings['Warehouse'] = {
onrender: function(node) {
if (node.data && node.data.balance!==undefined) {
$('<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);
}
}