Merge branch 'develop' into dev_fix_27359

This commit is contained in:
HENRY Florian 2021-09-30 11:08:44 +02:00 committed by GitHub
commit 0bc213ed17
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
176 changed files with 5043 additions and 6077 deletions

View File

@ -24,6 +24,8 @@ def docs_link_exists(body):
parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
elif parsed_url.netloc == "docs.erpnext.com":
return True
if __name__ == "__main__":

View File

@ -19,3 +19,16 @@ rules:
languages: [python]
severity: ERROR
- id: frappe-translated-values-in-business-logic
paths:
include:
- "**/report"
patterns:
- pattern-inside: |
{..., filters: [...], ...}
- pattern: |
{..., options: [..., __("..."), ...], ...}
message: |
Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
languages: [javascript]
severity: ERROR

View File

@ -99,34 +99,10 @@ jobs:
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
- name: Upload Coverage Data
run: |
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
coveralls:
name: Coverage Wrap Up
needs: test
container: python:3-slim
runs-on: ubuntu-latest
steps:
- name: Clone
uses: actions/checkout@v2
- name: Coveralls Finished
run: |
cd ${GITHUB_WORKSPACE}
pip3 install coverage==5.5
pip3 install coveralls==3.0.1
coveralls --finish
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Upload coverage data
uses: codecov/codecov-action@v2
with:
name: MariaDB
fail_ci_if_error: true
files: /home/runner/frappe-bench/sites/coverage.xml
verbose: true

8
.snyk
View File

@ -1,8 +0,0 @@
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
version: v1.14.0
ignore: {}
# patches apply the minimum changes required to fix a vulnerability
patch:
SNYK-JS-LODASH-450202:
- cypress > getos > async > lodash:
patched: '2020-01-31T01:35:12.802Z'

View File

@ -7,7 +7,7 @@
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
[![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
[https://erpnext.com](https://erpnext.com)

17
codecov.yml Normal file
View File

@ -0,0 +1,17 @@
codecov:
require_ci_to_pass: yes
coverage:
status:
project:
default:
target: auto
threshold: 0.5%
comment:
layout: "diff, files"
require_changes: true
after_n_builds: 3
ignore:
- "erpnext/demo"

View File

@ -374,12 +374,15 @@ def make_gl_entries(doc, credit_account, debit_account, against,
try:
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
frappe.db.commit()
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(message=traceback)
except Exception as e:
if frappe.flags.in_test:
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(message=traceback)
frappe.flags.deferred_accounting_error = True
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(deferred_process)

View File

@ -79,7 +79,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
} else {
generate_tree_preview(frm);
validate_csv_data(frm);
}
},
@ -104,23 +103,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
}
});
var validate_csv_data = function(frm) {
frappe.call({
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_accounts",
args: {file_name: frm.doc.import_file},
callback: function(r) {
if(r.message && r.message[0]===true) {
frm.page["show_import_button"] = true;
frm.page["total_accounts"] = r.message[1];
frm.trigger("refresh");
} else {
frm.page.set_indicator(__('Resolve error and upload again.'), 'orange');
frappe.throw(__(r.message));
}
}
});
};
var create_import_button = function(frm) {
frm.page.set_primary_action(__("Import"), function () {
frappe.call({
@ -151,23 +133,25 @@ var create_reset_button = function(frm) {
};
var generate_tree_preview = function(frm) {
let parent = __('All Accounts');
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
if (frm.doc.import_file) {
let parent = __('All Accounts');
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
// generate tree structure based on the csv data
new frappe.ui.Tree({
parent: $(frm.fields_dict['chart_tree'].wrapper),
label: parent,
expandable: true,
method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
args: {
file_name: frm.doc.import_file,
parent: parent,
doctype: 'Chart of Accounts Importer',
file_type: frm.doc.file_type
},
onclick: function(node) {
parent = node.value;
}
});
// generate tree structure based on the csv data
new frappe.ui.Tree({
parent: $(frm.fields_dict['chart_tree'].wrapper),
label: parent,
expandable: true,
method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
args: {
file_name: frm.doc.import_file,
parent: parent,
doctype: 'Chart of Accounts Importer',
file_type: frm.doc.file_type
},
onclick: function(node) {
parent = node.value;
}
});
}
};

View File

@ -25,8 +25,16 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
class ChartofAccountsImporter(Document):
def validate(self):
validate_accounts(self.import_file)
pass
def validate_columns(data):
if not data:
frappe.throw(_('No data found. Seems like you uploaded a blank file'))
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'))
@frappe.whitelist()
def validate_company(company):
@ -131,6 +139,8 @@ def get_coa(doctype, parent, is_root=False, file_name=None):
else:
data = generate_data_from_excel(file_doc, extension)
validate_columns(data)
validate_accounts(data)
forest = build_forest(data)
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
@ -322,9 +332,6 @@ def validate_accounts(file_name):
def validate_root(accounts):
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
if len(roots) < 4:
frappe.throw(_("Number of root accounts cannot be less than 4"))
error_messages = []
for account in roots:
@ -364,20 +371,12 @@ def get_mandatory_account_types():
def validate_account_types(accounts):
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1]
account_types = [accounts[d]["account_type"] for d in accounts if not cint(accounts[d]['is_group']) == 1]
missing = list(set(account_types_for_ledger) - set(account_types))
if missing:
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
account_types_for_group = ["Bank", "Cash", "Stock"]
# fix logic bug
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1]
missing = list(set(account_types_for_group) - set(account_groups))
if missing:
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
def unset_existing_data(company):
linked = frappe.db.sql('''select fieldname from tabDocField
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)

View File

@ -13,7 +13,7 @@ def get_data():
},
{
'label': _('References'),
'items': ['Period Closing Voucher', 'Tax Withholding Category']
'items': ['Period Closing Voucher']
},
{
'label': _('Target Details'),

View File

@ -1,63 +1,33 @@
{
"allow_copy": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2014-10-02 13:35:44.155278",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"actions": [],
"creation": "2014-10-02 13:35:44.155278",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"company"
],
"fields": [
{
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"fieldname": "company",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_list_view": 1,
"label": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
"fieldname": "company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company"
}
],
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2016-07-11 03:28:00.505946",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year Company",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_seen": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-28 18:01:53.495929",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Fiscal Year Company",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -13,10 +13,12 @@
"voucher_type",
"naming_series",
"finance_book",
"tax_withholding_category",
"column_break1",
"from_template",
"company",
"posting_date",
"apply_tds",
"2_add_edit_gl_entries",
"accounts",
"section_break99",
@ -498,16 +500,32 @@
"options": "Journal Entry Template",
"print_hide": 1,
"report_hide": 1
},
{
"depends_on": "eval:doc.apply_tds",
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
"label": "Tax Withholding Category",
"mandatory_depends_on": "eval:doc.apply_tds",
"options": "Tax Withholding Category"
},
{
"default": "0",
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2020-10-30 13:56:01.121995",
"modified": "2021-09-09 15:31:14.484029",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -15,6 +15,9 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
get_party_account_based_on_invoice_discounting,
)
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_party_tax_withholding_details,
)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
check_if_stock_and_account_balance_synced,
@ -57,7 +60,8 @@ class JournalEntry(AccountsController):
self.validate_against_jv()
self.validate_reference_doc()
self.set_against_account()
if self.docstatus == 0:
self.set_against_account()
self.create_remarks()
self.set_print_format_fields()
self.validate_expense_claim()
@ -66,6 +70,10 @@ class JournalEntry(AccountsController):
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
self.validate_stock_accounts()
if self.docstatus == 0:
self.apply_tax_withholding()
if not self.title:
self.title = self.get_title()
@ -139,6 +147,72 @@ class JournalEntry(AccountsController):
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
.format(account), StockAccountInvalidTransaction)
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'):
return
parties = [d.party for d in self.get('accounts') if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer'
doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice'
debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency'
rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency'
party_account = get_party_account(party_type.title(), parties[0], self.company)
net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account)
not in ('Tax', 'Chargeable'))
party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account)
inv = frappe._dict({
party_type: parties[0],
'doctype': doctype,
'company': self.company,
'posting_date': self.posting_date,
'net_total': net_total
})
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
if not tax_withholding_details:
return
accounts = []
for d in self.get('accounts'):
if d.get('account') == tax_withholding_details.get("account_head"):
d.update({
'account': tax_withholding_details.get("account_head"),
debit_or_credit: tax_withholding_details.get('tax_amount')
})
accounts.append(d.get('account'))
if d.get('account') == party_account:
d.update({
rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount')
})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append("accounts", {
'account': tax_withholding_details.get("account_head"),
rev_debit_or_credit: tax_withholding_details.get('tax_amount'),
'against_account': parties[0]
})
to_remove = [d for d in self.get('accounts')
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")]
for d in to_remove:
self.remove(d)
def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\

View File

@ -93,6 +93,7 @@
"options": "Payment Term"
},
{
"depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
@ -103,7 +104,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-21 13:30:11.605388",
"modified": "2021-09-26 17:06:55.597389",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",

View File

@ -7,6 +7,7 @@
"field_order": [
"reference_type",
"reference_name",
"reference_row",
"column_break_3",
"invoice_type",
"invoice_number",
@ -121,11 +122,17 @@
"label": "Amount",
"options": "Currency",
"read_only": 1
},
{
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Row"
}
],
"istable": 1,
"links": [],
"modified": "2021-08-30 10:58:42.665107",
"modified": "2021-09-20 17:23:09.455803",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",

View File

@ -40,6 +40,7 @@ class POSInvoice(SalesInvoice):
self.validate_change_amount()
self.validate_change_account()
self.validate_item_cost_centers()
self.validate_warehouse()
self.validate_serialised_or_batched_item()
self.validate_stock_availablility()
self.validate_return_items_qty()

View File

@ -4,7 +4,7 @@
frappe.ui.form.on('POS Invoice Merge Log', {
setup: function(frm) {
frm.set_query("pos_invoice", "pos_invoices", doc => {
return{
return {
filters: {
'docstatus': 1,
'customer': doc.customer,
@ -12,5 +12,10 @@ frappe.ui.form.on('POS Invoice Merge Log', {
}
}
});
},
merge_invoices_based_on: function(frm) {
frm.set_value('customer', '');
frm.set_value('customer_group', '');
}
});

View File

@ -6,9 +6,11 @@
"engine": "InnoDB",
"field_order": [
"posting_date",
"customer",
"merge_invoices_based_on",
"column_break_3",
"pos_closing_entry",
"customer",
"customer_group",
"section_break_3",
"pos_invoices",
"references_section",
@ -88,12 +90,27 @@
"fieldtype": "Link",
"label": "POS Closing Entry",
"options": "POS Closing Entry"
},
{
"fieldname": "merge_invoices_based_on",
"fieldtype": "Select",
"label": "Merge Invoices Based On",
"options": "Customer\nCustomer Group",
"reqd": 1
},
{
"depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"fieldname": "customer_group",
"fieldtype": "Link",
"label": "Customer Group",
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"options": "Customer Group"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-12-01 11:53:57.267579",
"modified": "2021-09-14 11:17:19.001142",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",

View File

@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
self.validate_pos_invoice_status()
def validate_customer(self):
if self.merge_invoices_based_on == 'Customer Group':
return
for d in self.pos_invoices:
if d.customer != self.customer:
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
@ -124,7 +127,7 @@ class POSInvoiceMergeLog(Document):
found = False
for i in items:
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
i.uom == item.uom and i.net_rate == item.net_rate):
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
found = True
i.qty = i.qty + item.qty
@ -172,6 +175,11 @@ class POSInvoiceMergeLog(Document):
invoice.discount_amount = 0.0
invoice.taxes_and_charges = None
invoice.ignore_pricing_rule = 1
invoice.customer = self.customer
if self.merge_invoices_based_on == 'Customer Group':
invoice.flags.ignore_pos_profile = True
invoice.pos_profile = ''
return invoice
@ -228,7 +236,7 @@ def get_all_unconsolidated_invoices():
return pos_invoices
def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
pos_invoice_customer_map = {}
for invoice in pos_invoices:
customer = invoice.get('customer')

View File

@ -219,6 +219,7 @@
},
{
"default": "1",
"description": "A customer must have primary contact email.",
"fieldname": "primary_mandatory",
"fieldtype": "Check",
"label": "Send To Primary Contact"
@ -286,10 +287,11 @@
}
],
"links": [],
"modified": "2021-05-21 11:14:22.426672",
"modified": "2021-09-06 21:00:45.732505",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
"naming_rule": "Set by user",
"owner": "Administrator",
"permissions": [
{

View File

@ -196,7 +196,10 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
primary_email = customer.get('email_id') or ''
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
if int(primary_mandatory):
if (primary_email == ''):
continue
elif (billing_email == '') and (primary_email == ''):
continue
customer_list.append({
@ -208,10 +211,29 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
@frappe.whitelist()
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
""" Returns first email from Contact Email table as a Billing email
when Is Billing Contact checked
and Primary email- email with Is Primary checked """
billing_email = frappe.db.sql("""
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
order by c.creation desc""", customer_name)
SELECT
email.email_id
FROM
`tabContact Email` AS email
JOIN
`tabDynamic Link` AS link
ON
email.parent=link.parent
JOIN
`tabContact` AS contact
ON
contact.name=link.parent
WHERE
link.link_doctype='Customer'
and link.link_name=%s
and contact.is_billing_contact=1
ORDER BY
contact.creation desc""", customer_name)
if len(billing_email) == 0 or (billing_email[0][0] is None):
if billing_and_primary:

View File

@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
is_overdue,
unlink_inter_company_doc,
update_linked_doc,
validate_inter_company_party,
@ -1145,6 +1146,12 @@ class PurchaseInvoice(BuyingController):
if not self.apply_tds:
return
if self.apply_tds and not self.get('tax_withholding_category'):
self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category')
if not self.tax_withholding_category:
return
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
if not tax_withholding_details:
@ -1175,10 +1182,7 @@ class PurchaseInvoice(BuyingController):
self.status = 'Draft'
return
precision = self.precision("outstanding_amount")
outstanding_amount = flt(self.outstanding_amount, precision)
due_date = getdate(self.due_date)
nowdate = getdate()
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
if not status:
if self.docstatus == 2:
@ -1186,9 +1190,11 @@ class PurchaseInvoice(BuyingController):
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate:
elif is_overdue(self):
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate:
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
#Check if outstanding amount is 0 due to debit note issued against invoice
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):

View File

@ -2,28 +2,58 @@
// License: GNU General Public License v3. See license.txt
// render
frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
get_indicator: function(doc) {
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
if(cint(doc.on_hold) && !doc.release_date) {
return [__("On Hold"), "darkgrey"];
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
return [__("Temporarily on Hold"), "darkgrey"];
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
}
} else if (cint(doc.is_return)) {
return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
return [__("Paid"), "green", "outstanding_amount,=,0"];
frappe.listview_settings["Purchase Invoice"] = {
add_fields: [
"supplier",
"supplier_name",
"base_grand_total",
"outstanding_amount",
"due_date",
"company",
"currency",
"is_return",
"release_date",
"on_hold",
"represents_company",
"is_internal_supplier",
],
get_indicator(doc) {
if (doc.status == "Debit Note Issued") {
return [__(doc.status), "darkgrey", "status,=," + doc.status];
}
}
if (
flt(doc.outstanding_amount) > 0 &&
doc.docstatus == 1 &&
cint(doc.on_hold)
) {
if (!doc.release_date) {
return [__("On Hold"), "darkgrey"];
} else if (
frappe.datetime.get_diff(
doc.release_date,
frappe.datetime.nowdate()
) > 0
) {
return [__("Temporarily on Hold"), "darkgrey"];
}
}
const status_colors = {
"Unpaid": "orange",
"Paid": "green",
"Return": "gray",
"Overdue": "red",
"Partly Paid": "yellow",
"Internal Transfer": "darkgrey",
};
if (status_colors[doc.status]) {
return [
__(doc.status),
status_colors[doc.status],
"status,=," + doc.status,
];
}
},
};

View File

@ -1151,10 +1151,11 @@ class TestPurchaseInvoice(unittest.TestCase):
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
# Update tax withholding category with current fiscal year and rate details
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC')
# Create Purchase Order with TDS applied
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
posting_date='2021-09-15')
po.apply_tds = 1
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
po.save()
@ -1226,16 +1227,20 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
def update_tax_witholding_category(company, account, date):
def update_tax_witholding_category(company, account):
from erpnext.accounts.utils import get_fiscal_year
fiscal_year = get_fiscal_year(date=date, company=company)
fiscal_year = get_fiscal_year(fiscal_year='2021')
if not frappe.db.get_value('Tax Withholding Rate',
{'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
'to_date': ('<=', fiscal_year[2])}):
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
tds_category.set('rates', [])
tds_category.append('rates', {
'fiscal_year': fiscal_year[0],
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 2500,
'cumulative_threshold': 0

View File

@ -97,6 +97,7 @@
"width": "100px"
},
{
"depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
@ -104,6 +105,7 @@
"read_only": 1
},
{
"depends_on": "exchange_gain_loss",
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
@ -115,7 +117,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-20 16:26:53.820530",
"modified": "2021-09-26 15:47:28.167371",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Advance",

View File

@ -445,15 +445,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
this.frm.refresh_field("base_paid_amount");
}
currency() {
this._super();
$.each(cur_frm.doc.timesheets, function(i, d) {
let row = frappe.get_doc(d.doctype, d.name)
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
});
calculate_total_billing_amount(cur_frm)
}
currency() {
var me = this;
super.currency();

View File

@ -247,7 +247,7 @@
"depends_on": "customer",
"fetch_from": "customer.customer_name",
"fieldname": "customer_name",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"in_global_search": 1,
@ -1061,6 +1061,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Apply Additional Discount On",
"length": 15,
"options": "\nGrand Total\nNet Total",
"print_hide": 1
},
@ -1147,7 +1148,7 @@
{
"description": "In Words will be visible once you save the Sales Invoice.",
"fieldname": "base_in_words",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words (Company Currency)",
@ -1207,7 +1208,7 @@
},
{
"fieldname": "in_words",
"fieldtype": "Data",
"fieldtype": "Small Text",
"hide_days": 1,
"hide_seconds": 1,
"label": "In Words",
@ -1560,6 +1561,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Print Language",
"length": 6,
"print_hide": 1,
"read_only": 1
},
@ -1647,8 +1649,9 @@
"hide_seconds": 1,
"in_standard_filter": 1,
"label": "Status",
"length": 30,
"no_copy": 1,
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nUnpaid and Discounted\nPartly Paid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
"read_only": 1
},
@ -1706,6 +1709,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Is Opening Entry",
"length": 4,
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
@ -1717,6 +1721,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "C-Form Applicable",
"length": 4,
"no_copy": 1,
"options": "No\nYes",
"print_hide": 1
@ -1948,6 +1953,7 @@
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@ -2017,11 +2023,12 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-08-27 20:13:40.456462",
"modified": "2021-09-28 13:09:34.391799",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -499,7 +499,7 @@ class SalesInvoice(SellingController):
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
if not self.pos_profile:
if not self.pos_profile and not self.flags.ignore_pos_profile:
pos_profile = get_pos_profile(self.company) or {}
if not pos_profile:
return
@ -1475,14 +1475,7 @@ class SalesInvoice(SellingController):
self.status = 'Draft'
return
precision = self.precision("outstanding_amount")
outstanding_amount = flt(self.outstanding_amount, precision)
due_date = getdate(self.due_date)
nowdate = getdate()
discounting_status = None
if self.is_discounted:
discounting_status = get_discounting_status(self.name)
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
if not status:
if self.docstatus == 2:
@ -1490,15 +1483,13 @@ class SalesInvoice(SellingController):
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discounting_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
elif is_overdue(self):
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discounting_status=='Disbursed':
self.status = "Unpaid and Discounted"
elif outstanding_amount > 0 and due_date >= nowdate:
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
#Check if outstanding amount is 0 due to credit note issued against invoice
# Check if outstanding amount is 0 due to credit note issued against invoice
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
self.status = "Credit Note Issued"
elif self.is_return == 1:
@ -1507,12 +1498,42 @@ class SalesInvoice(SellingController):
self.status = "Paid"
else:
self.status = "Submitted"
if (
self.status in ("Unpaid", "Partly Paid", "Overdue")
and self.is_discounted
and get_discounting_status(self.name) == "Disbursed"
):
self.status += " and Discounted"
else:
self.status = "Draft"
if update:
self.db_set('status', self.status, update_modified = update_modified)
def is_overdue(doc):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
if outstanding_amount <= 0:
return
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
nowdate = getdate()
if doc.payment_schedule:
# calculate payable amount till date
payable_amount = sum(
payment.payment_amount
for payment in doc.payment_schedule
if getdate(payment.due_date) < nowdate
)
if (grand_total - outstanding_amount) < payable_amount:
return True
elif getdate(doc.due_date) < nowdate:
return True
def get_discounting_status(sales_invoice):
status = None

View File

@ -6,18 +6,20 @@ frappe.listview_settings['Sales Invoice'] = {
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
"currency", "is_return"],
get_indicator: function(doc) {
var status_color = {
const status_colors = {
"Draft": "grey",
"Unpaid": "orange",
"Paid": "green",
"Return": "gray",
"Credit Note Issued": "gray",
"Unpaid and Discounted": "orange",
"Partly Paid and Discounted": "yellow",
"Overdue and Discounted": "red",
"Overdue": "red",
"Partly Paid": "yellow",
"Internal Transfer": "darkgrey"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
},
right_column: "grand_total"
};

View File

@ -133,6 +133,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_payment_entry_unlink_against_invoice(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si = frappe.copy_doc(test_records[0])
si.is_pos = 0
si.insert()
@ -156,6 +157,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_payment_entry_unlink_against_standalone_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si1 = create_sales_invoice(rate=1000)
si2 = create_sales_invoice(rate=300)
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
@ -1420,15 +1422,22 @@ class TestSalesInvoice(unittest.TestCase):
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
expected_itemised_tax = {
"999800": {
"_Test Item": {
"Service Tax": {
"tax_rate": 10.0,
"tax_amount": 1500.0
"tax_amount": 1000.0
}
},
"_Test Item 2": {
"Service Tax": {
"tax_rate": 10.0,
"tax_amount": 500.0
}
}
}
expected_itemised_taxable_amount = {
"999800": 15000.0
"_Test Item": 10000.0,
"_Test Item 2": 5000.0
}
self.assertEqual(itemised_tax, expected_itemised_tax)
@ -1639,6 +1648,7 @@ class TestSalesInvoice(unittest.TestCase):
def test_credit_note(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
outstanding_amount = get_outstanding_amount(si.doctype,
@ -1790,6 +1800,47 @@ class TestSalesInvoice(unittest.TestCase):
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
frappe.set_user("Administrator")
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
item = create_item("_Test Item for Deferred Accounting")
item.enable_deferred_revenue = 1
item.deferred_revenue_account = deferred_account
item.no_of_months = 12
item.save()
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
si.items[0].deferred_revenue_account = deferred_account
si.save()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
pda1 = frappe.get_doc(dict(
doctype='Process Deferred Accounting',
posting_date=nowdate(),
start_date="2019-01-01",
end_date="2019-03-31",
type="Income",
company="_Test Company"
))
pda1.insert()
self.assertRaises(frappe.ValidationError, pda1.submit)
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
def test_fixed_deferred_revenue(self):
deferred_account = create_account(account_name="Deferred Revenue",
parent_account="Current Liabilities - _TC", company="_Test Company")
@ -2262,6 +2313,54 @@ class TestSalesInvoice(unittest.TestCase):
party_link.delete()
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
def test_payment_statuses(self):
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
today = nowdate()
# Test Overdue
si = create_sales_invoice(do_not_submit=True)
si.payment_schedule = []
si.append("payment_schedule", {
"due_date": add_days(today, -5),
"invoice_portion": 50,
"payment_amount": si.grand_total / 2
})
si.append("payment_schedule", {
"due_date": add_days(today, 5),
"invoice_portion": 50,
"payment_amount": si.grand_total / 2
})
si.submit()
self.assertEqual(si.status, "Overdue")
# Test payment less than due amount
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_amount = 1
pe.references[0].allocated_amount = pe.paid_amount
pe.submit()
si.reload()
self.assertEqual(si.status, "Overdue")
# Test Partly Paid
pe = frappe.copy_doc(pe)
pe.paid_amount = si.grand_total / 2
pe.references[0].allocated_amount = pe.paid_amount
pe.submit()
si.reload()
self.assertEqual(si.status, "Partly Paid")
# Test Paid
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
pe.reference_no = "1"
pe.reference_date = nowdate()
pe.paid_amount = si.outstanding_amount
pe.submit()
si.reload()
self.assertEqual(si.status, "Paid")
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
@ -2294,6 +2393,7 @@ def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_line2": "_Test Address Line 2",
"address_title": "_Test Address for Eway bill",
"address_type": "Billing",
"city": "_Test City",
@ -2315,11 +2415,12 @@ def make_test_address_for_ewaybill():
address.save()
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Billing'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_line2": "_Test Address Line 2",
"address_title": "_Test Customer-Address for Eway bill",
"address_type": "Shipping",
"address_type": "Billing",
"city": "_Test City",
"state": "Test State",
"country": "India",
@ -2339,9 +2440,34 @@ def make_test_address_for_ewaybill():
address.save()
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Address Line 1",
"address_line2": "_Test Address Line 2",
"address_title": "_Test Customer-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
"state": "Test State",
"country": "India",
"doctype": "Address",
"is_primary_address": 1,
"phone": "+910000000000",
"gst_state": "Maharashtra",
"gst_state_number": "27",
"pincode": "410098"
}).insert()
address.append("links", {
"link_doctype": "Customer",
"link_name": "_Test Customer"
})
address.save()
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
address = frappe.get_doc({
"address_line1": "_Test Dispatch Address Line 1",
"address_line2": "_Test Dispatch Address Line 2",
"address_title": "_Test Dispatch-Address for Eway bill",
"address_type": "Shipping",
"city": "_Test City",
@ -2356,11 +2482,6 @@ def make_test_address_for_ewaybill():
"pincode": "1100101"
}).insert()
address.append("links", {
"link_doctype": "Company",
"link_name": "_Test Company"
})
address.save()
def make_test_transporter_for_ewaybill():
@ -2400,7 +2521,8 @@ def make_sales_invoice_for_ewaybill():
si.distance = 2000
si.company_address = "_Test Address for Eway bill-Billing"
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
si.customer_address = "_Test Customer-Address for Eway bill-Billing"
si.shipping_address_name = "_Test Customer-Address for Eway bill-Shipping"
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
si.vehicle_no = "KA12KA1234"
si.gst_category = "Registered Regular"

View File

@ -98,6 +98,7 @@
"width": "120px"
},
{
"depends_on": "exchange_gain_loss",
"fieldname": "exchange_gain_loss",
"fieldtype": "Currency",
"label": "Exchange Gain/Loss",
@ -105,6 +106,7 @@
"read_only": 1
},
{
"depends_on": "exchange_gain_loss",
"fieldname": "ref_exchange_rate",
"fieldtype": "Float",
"label": "Reference Exchange Rate",
@ -116,7 +118,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-06-04 20:25:49.832052",
"modified": "2021-09-26 15:47:46.911595",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Advance",

View File

@ -400,6 +400,7 @@ class Subscription(Document):
invoice.flags.ignore_mandatory = True
invoice.set_missing_values()
invoice.save()
if self.submit_invoice:

View File

@ -9,11 +9,26 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import cint, getdate
from erpnext.accounts.utils import get_fiscal_year
class TaxWithholdingCategory(Document):
pass
def validate(self):
self.validate_dates()
self.validate_thresholds()
def validate_dates(self):
last_date = None
for d in self.get('rates'):
if getdate(d.from_date) >= getdate(d.to_date):
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
# validate overlapping of dates
if last_date and getdate(d.to_date) < getdate(last_date):
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
def validate_thresholds(self):
for d in self.get('rates'):
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx))
def get_party_details(inv):
party_type, party = '', ''
@ -52,8 +67,8 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if not parties:
parties.append(party)
fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
posting_date = inv.get('posting_date') or inv.get('transaction_date')
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
@ -67,7 +82,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
tax_amount, tax_deducted = get_tax_amount(
party_type, parties,
inv, tax_details,
fiscal_year, pan_no
posting_date, pan_no
)
if party_type == 'Supplier':
@ -77,16 +92,19 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
return tax_row
def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
tax_rate_detail = get_tax_withholding_rates(tax_withholding, fiscal_year)
tax_rate_detail = get_tax_withholding_rates(tax_withholding, posting_date)
for account_detail in tax_withholding.accounts:
if company == account_detail.company:
return frappe._dict({
"tax_withholding_category": tax_withholding_category,
"account_head": account_detail.account,
"rate": tax_rate_detail.tax_withholding_rate,
"from_date": tax_rate_detail.from_date,
"to_date": tax_rate_detail.to_date,
"threshold": tax_rate_detail.single_threshold,
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
@ -95,13 +113,13 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
"round_off_tax_amount": tax_withholding.round_off_tax_amount
})
def get_tax_withholding_rates(tax_withholding, fiscal_year):
def get_tax_withholding_rates(tax_withholding, posting_date):
# returns the row that matches with the fiscal year from posting date
for rate in tax_withholding.rates:
if rate.fiscal_year == fiscal_year:
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
return rate
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
frappe.throw(_("No Tax Withholding data found for the current posting date."))
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
row = {
@ -143,38 +161,38 @@ def get_tax_row_for_tds(tax_details, tax_amount):
"account_head": tax_details.account_head
}
def get_lower_deduction_certificate(fiscal_year, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
def get_lower_deduction_certificate(tax_details, pan_no):
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
{
'pan_no': pan_no,
'valid_from': ('>=', tax_details.from_date),
'valid_upto': ('<=', tax_details.to_date)
}, 'name')
if ldc_name:
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
fiscal_year = fiscal_year_details[0]
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
to_date=tax_details.to_date, party_type=party_type)
taxable_vouchers = vouchers + advance_vouchers
tax_deducted = 0
if taxable_vouchers:
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
tax_amount = 0
posting_date = inv.get('posting_date') or inv.get('transaction_date')
if party_type == 'Supplier':
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
ldc = get_lower_deduction_certificate(tax_details, pan_no)
if tax_deducted:
net_total = inv.net_total
if ldc:
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total)
else:
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
else:
tax_amount = get_tds_amount(
ldc, parties, inv, tax_details,
fiscal_year_details, tax_deducted, vouchers
)
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
elif party_type == 'Customer':
if tax_deducted:
@ -183,29 +201,47 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
else:
# if no TCS has been charged in FY,
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(
parties, inv, tax_details,
fiscal_year_details, vouchers, advance_vouchers
)
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice'
filters = {
dr_or_cr: ['>', 0],
'company': company,
'party_type': party_type,
'party': ['in', parties],
'fiscal_year': fiscal_year,
frappe.scrub(party_type): ['in', parties],
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'is_opening': 'No',
'is_cancelled': 0
'docstatus': 1
}
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice":
filters.update({
'apply_tds': 1,
'tax_withholding_category': tax_details.get('tax_withholding_category')
})
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
journal_entries = frappe.db.sql("""
SELECT j.name
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
WHERE
j.docstatus = 1
AND j.is_opening = 'No'
AND j.posting_date between %s and %s
AND ja.{dr_or_cr} > 0
AND ja.party in %s
""".format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1)
if journal_entries:
journal_entries = journal_entries[0]
return invoices + journal_entries
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'):
# for advance vouchers, debit and credit is reversed
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
@ -218,8 +254,6 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
'against_voucher': ['is', 'not set']
}
if fiscal_year:
filters['fiscal_year'] = fiscal_year
if company:
filters['company'] = company
if from_date and to_date:
@ -227,20 +261,21 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
def get_deducted_tax(taxable_vouchers, tax_details):
# check if TDS / TCS account is already charged on taxable vouchers
filters = {
'is_cancelled': 0,
'credit': ['>', 0],
'fiscal_year': fiscal_year,
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'account': tax_details.account_head,
'voucher_no': ['in', taxable_vouchers],
}
field = "sum(credit)"
field = "credit"
return frappe.db.get_value('GL Entry', filters, field) or 0.0
entries = frappe.db.get_all('GL Entry', filters, pluck=field)
return sum(entries)
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
tds_amount = 0
invoice_filters = {
'name': ('in', vouchers),
@ -264,7 +299,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
supp_credit_amt += supp_jv_credit_amt
supp_credit_amt += inv.net_total
debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company)
supp_credit_amt -= debit_note_amount
threshold = tax_details.get('threshold', 0)
@ -292,9 +327,8 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
return tds_amount
def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0
fiscal_year, _, _ = fiscal_year_details
# sum of debit entries made from sales invoices
invoiced_amt = frappe.db.get_value('GL Entry', {
@ -313,14 +347,14 @@ def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv
}, 'sum(credit)') or 0.0
# sum of credit entries made from sales invoice
credit_note_amt = frappe.db.get_value('GL Entry', {
credit_note_amt = sum(frappe.db.get_all('GL Entry', {
'is_cancelled': 0,
'credit': ['>', 0],
'party': ['in', parties],
'fiscal_year': fiscal_year,
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
'company': inv.company,
'voucher_type': 'Sales Invoice',
}, 'sum(credit)') or 0.0
}, pluck='credit'))
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
@ -339,7 +373,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
return inv.grand_total - tcs_tax_row_amount
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total):
tds_amount = 0
limit_consumed = frappe.db.get_value('Purchase Invoice', {
'supplier': ('in', parties),
@ -356,14 +390,13 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
return tds_amount
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
_, year_start_date, year_end_date = fiscal_year_details
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
filters = {
'supplier': ['in', suppliers],
'is_return': 1,
'docstatus': 1,
'posting_date': ['between', (year_start_date, year_end_date)]
'posting_date': ['between', (from_date, to_date)]
}
fields = ['abs(sum(net_total)) as net_total']

View File

@ -176,6 +176,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices:
d.cancel()
def test_multi_category_single_supplier(self):
frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category")
invoices = []
pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True)
pi.tax_withholding_category = "Test Service Category"
pi.save()
pi.submit()
invoices.append(pi)
# Second Invoice will apply TDS checked
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True)
pi1.tax_withholding_category = "Test Goods Category"
pi1.save()
pi1.submit()
invoices.append(pi1)
self.assertEqual(pi1.taxes[0].tax_amount, 250)
#delete invoices to avoid clashing
for d in invoices:
d.cancel()
def cancel_invoices():
purchase_invoices = frappe.get_all("Purchase Invoice", {
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
@ -251,7 +274,8 @@ def create_sales_invoice(**args):
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3',
'Test TDS Supplier4', 'Test TDS Supplier5']:
if frappe.db.exists('Supplier', name):
continue
@ -313,16 +337,16 @@ def create_records():
}).insert()
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
# Cummulative thresold
fiscal_year = get_fiscal_year(today(), company="_Test Company")
# Cumulative threshold
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "Cumulative Threshold TDS",
"category_name": "10% TDS",
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000.00
@ -339,7 +363,8 @@ def create_tax_with_holding_category():
"name": "Cumulative Threshold TCS",
"category_name": "10% TCS",
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000.00
@ -357,7 +382,8 @@ def create_tax_with_holding_category():
"name": "Single Threshold TDS",
"category_name": "10% TDS",
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 20000.00,
'cumulative_threshold': 0
@ -377,7 +403,8 @@ def create_tax_with_holding_category():
"consider_party_ledger_amount": 1,
"tax_on_excess_amount": 1,
"rates": [{
'fiscal_year': fiscal_year,
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 0,
'cumulative_threshold': 30000
@ -387,3 +414,39 @@ def create_tax_with_holding_category():
'account': 'TDS - _TC'
}]
}).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Service Category"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "Test Service Category",
"category_name": "Test Service Category",
"rates": [{
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 2000,
'cumulative_threshold': 2000
}],
"accounts": [{
'company': '_Test Company',
'account': 'TDS - _TC'
}]
}).insert()
if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"):
frappe.get_doc({
"doctype": "Tax Withholding Category",
"name": "Test Goods Category",
"category_name": "Test Goods Category",
"rates": [{
'from_date': fiscal_year[1],
'to_date': fiscal_year[2],
'tax_withholding_rate': 10,
'single_threshold': 2000,
'cumulative_threshold': 2000
}],
"accounts": [{
'company': '_Test Company',
'account': 'TDS - _TC'
}]
}).insert()

View File

@ -1,202 +1,72 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-07-17 16:53:13.716665",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2018-07-17 16:53:13.716665",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"from_date",
"to_date",
"tax_withholding_rate",
"column_break_3",
"single_threshold",
"cumulative_threshold"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "fiscal_year",
"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": "Fiscal Year",
"length": 0,
"no_copy": 0,
"options": "Fiscal Year",
"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,
"translatable": 0,
"unique": 0
},
"columns": 1,
"fieldname": "tax_withholding_rate",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Tax Withholding Rate",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 2,
"fieldname": "tax_withholding_rate",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Tax Withholding Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 2,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Single Transaction Threshold"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "single_threshold",
"fieldtype": "Currency",
"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": "Single Transaction Threshold",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Cumulative Transaction Threshold"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 3,
"fieldname": "cumulative_threshold",
"fieldtype": "Currency",
"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": "Cumulative Transaction Threshold",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"columns": 2,
"fieldname": "from_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "From Date",
"reqd": 1
},
{
"columns": 2,
"fieldname": "to_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "To Date",
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-07-17 17:13:09.819580",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
"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_views": 0
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-08-31 11:42:12.213977",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Withholding Rate",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -284,13 +284,16 @@ def check_freezing_date(posting_date, adv_adj=False):
"""
Nobody can do GL Entries where posting date is before freezing date
except authorized person
Administrator has all the roles so this check will be bypassed if any role is allowed to post
Hence stop admin to bypass if accounts are freezed
"""
if not adv_adj:
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
if getdate(posting_date) <= getdate(acc_frozen_upto) \
and not frozen_accounts_modifier in frappe.get_roles():
and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
def set_as_cancel(voucher_type, voucher_no):

View File

@ -4,7 +4,7 @@
frappe.query_reports["Accounts Payable"] = {
"filters": [
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@ -12,19 +12,19 @@ frappe.query_reports["Accounts Payable"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"fieldname": "report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"finance_book",
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@ -38,7 +38,7 @@ frappe.query_reports["Accounts Payable"] = {
}
},
{
"fieldname":"supplier",
"fieldname": "supplier",
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
@ -54,48 +54,48 @@ frappe.query_reports["Accounts Payable"] = {
}
},
{
"fieldname":"ageing_based_on",
"fieldname": "ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
"default": "Due Date"
},
{
"fieldname":"range1",
"fieldname": "range1",
"label": __("Ageing Range 1"),
"fieldtype": "Int",
"default": "30",
"reqd": 1
},
{
"fieldname":"range2",
"fieldname": "range2",
"label": __("Ageing Range 2"),
"fieldtype": "Int",
"default": "60",
"reqd": 1
},
{
"fieldname":"range3",
"fieldname": "range3",
"label": __("Ageing Range 3"),
"fieldtype": "Int",
"default": "90",
"reqd": 1
},
{
"fieldname":"range4",
"fieldname": "range4",
"label": __("Ageing Range 4"),
"fieldtype": "Int",
"default": "120",
"reqd": 1
},
{
"fieldname":"payment_terms_template",
"fieldname": "payment_terms_template",
"label": __("Payment Terms Template"),
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"supplier_group",
"fieldname": "supplier_group",
"label": __("Supplier Group"),
"fieldtype": "Link",
"options": "Supplier Group"
@ -106,12 +106,17 @@ frappe.query_reports["Accounts Payable"] = {
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"fieldname": "based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname":"tax_id",
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1

View File

@ -4,7 +4,7 @@
frappe.query_reports["Accounts Receivable"] = {
"filters": [
{
"fieldname":"company",
"fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@ -12,19 +12,19 @@ frappe.query_reports["Accounts Receivable"] = {
"default": frappe.defaults.get_user_default("Company")
},
{
"fieldname":"report_date",
"fieldname": "report_date",
"label": __("Posting Date"),
"fieldtype": "Date",
"default": frappe.datetime.get_today()
},
{
"fieldname":"finance_book",
"fieldname": "finance_book",
"label": __("Finance Book"),
"fieldtype": "Link",
"options": "Finance Book"
},
{
"fieldname":"cost_center",
"fieldname": "cost_center",
"label": __("Cost Center"),
"fieldtype": "Link",
"options": "Cost Center",
@ -38,7 +38,7 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"customer",
"fieldname": "customer",
"label": __("Customer"),
"fieldtype": "Link",
"options": "Customer",
@ -67,66 +67,66 @@ frappe.query_reports["Accounts Receivable"] = {
}
},
{
"fieldname":"ageing_based_on",
"fieldname": "ageing_based_on",
"label": __("Ageing Based On"),
"fieldtype": "Select",
"options": 'Posting Date\nDue Date',
"default": "Due Date"
},
{
"fieldname":"range1",
"fieldname": "range1",
"label": __("Ageing Range 1"),
"fieldtype": "Int",
"default": "30",
"reqd": 1
},
{
"fieldname":"range2",
"fieldname": "range2",
"label": __("Ageing Range 2"),
"fieldtype": "Int",
"default": "60",
"reqd": 1
},
{
"fieldname":"range3",
"fieldname": "range3",
"label": __("Ageing Range 3"),
"fieldtype": "Int",
"default": "90",
"reqd": 1
},
{
"fieldname":"range4",
"fieldname": "range4",
"label": __("Ageing Range 4"),
"fieldtype": "Int",
"default": "120",
"reqd": 1
},
{
"fieldname":"customer_group",
"fieldname": "customer_group",
"label": __("Customer Group"),
"fieldtype": "Link",
"options": "Customer Group"
},
{
"fieldname":"payment_terms_template",
"fieldname": "payment_terms_template",
"label": __("Payment Terms Template"),
"fieldtype": "Link",
"options": "Payment Terms Template"
},
{
"fieldname":"sales_partner",
"fieldname": "sales_partner",
"label": __("Sales Partner"),
"fieldtype": "Link",
"options": "Sales Partner"
},
{
"fieldname":"sales_person",
"fieldname": "sales_person",
"label": __("Sales Person"),
"fieldtype": "Link",
"options": "Sales Person"
},
{
"fieldname":"territory",
"fieldname": "territory",
"label": __("Territory"),
"fieldtype": "Link",
"options": "Territory"
@ -137,45 +137,50 @@ frappe.query_reports["Accounts Receivable"] = {
"fieldtype": "Check"
},
{
"fieldname":"based_on_payment_terms",
"fieldname": "based_on_payment_terms",
"label": __("Based On Payment Terms"),
"fieldtype": "Check",
},
{
"fieldname":"show_future_payments",
"fieldname": "show_future_payments",
"label": __("Show Future Payments"),
"fieldtype": "Check",
},
{
"fieldname":"show_delivery_notes",
"fieldname": "show_delivery_notes",
"label": __("Show Linked Delivery Notes"),
"fieldtype": "Check",
},
{
"fieldname":"show_sales_person",
"fieldname": "show_sales_person",
"label": __("Show Sales Person"),
"fieldtype": "Check",
},
{
"fieldname":"tax_id",
"fieldname": "show_remarks",
"label": __("Show Remarks"),
"fieldtype": "Check",
},
{
"fieldname": "tax_id",
"label": __("Tax Id"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"customer_name",
"fieldname": "customer_name",
"label": __("Customer Name"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"payment_terms",
"fieldname": "payment_terms",
"label": __("Payment Tems"),
"fieldtype": "Data",
"hidden": 1
},
{
"fieldname":"credit_limit",
"fieldname": "credit_limit",
"label": __("Credit Limit"),
"fieldtype": "Currency",
"hidden": 1

View File

@ -106,6 +106,7 @@ class ReceivablePayableReport(object):
party = gle.party,
posting_date = gle.posting_date,
account_currency = gle.account_currency,
remarks = gle.remarks if self.filters.get("show_remarks") else None,
invoiced = 0.0,
paid = 0.0,
credit_note = 0.0,
@ -583,10 +584,12 @@ class ReceivablePayableReport(object):
else:
select_fields = "debit, credit"
remarks = ", remarks" if self.filters.get("show_remarks") else ""
self.gl_entries = frappe.db.sql("""
select
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
against_voucher_type, against_voucher, account_currency, {0}
against_voucher_type, against_voucher, account_currency, {0} {remarks}
from
`tabGL Entry`
where
@ -595,7 +598,7 @@ class ReceivablePayableReport(object):
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
.format(select_fields, date_condition, conditions, order_by), values, as_dict=True)
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
def get_sales_invoices_or_customers_based_on_sales_person(self):
if self.filters.get("sales_person"):
@ -754,6 +757,10 @@ class ReceivablePayableReport(object):
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
options='voucher_type', width=180)
if self.filters.show_remarks:
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200),
self.add_column(label='Due Date', fieldtype='Date')
if self.party_type == "Supplier":

View File

@ -260,7 +260,12 @@ def get_company_currency(filters=None):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values():
for entry in entries:
d = accounts_by_name.get(entry.account_name)
if entry.account_number:
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
d = accounts_by_name.get(account_name)
if d:
for company in companies:
# check if posting date is within the period
@ -307,7 +312,14 @@ def update_parent_account_names(accounts):
of account_number and suffix of company abbr. This function adds key called
`parent_account_name` which does not have such prefix/suffix.
"""
name_to_account_map = { d.name : d.account_name for d in accounts }
name_to_account_map = {}
for d in accounts:
if d.account_number:
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
name_to_account_map[d.name] = account_name
for account in accounts:
if account.parent_account:
@ -420,7 +432,11 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries:
account_name = entry.account_name
if entry.account_number:
account_name = entry.account_number + ' - ' + entry.account_name
else:
account_name = entry.account_name
validate_entries(account_name, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(account_name, []).append(entry)
@ -491,7 +507,12 @@ def filter_accounts(accounts, depth=10):
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
accounts_by_name[d.account_name] = d
if d.account_number:
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = []

View File

@ -110,9 +110,26 @@ frappe.query_reports["General Ledger"] = {
"fieldname":"group_by",
"label": __("Group by"),
"fieldtype": "Select",
"options": ["", __("Group by Voucher"), __("Group by Voucher (Consolidated)"),
__("Group by Account"), __("Group by Party")],
"default": __("Group by Voucher (Consolidated)")
"options": [
"",
{
label: __("Group by Voucher"),
value: "Group by Voucher",
},
{
label: __("Group by Voucher (Consolidated)"),
value: "Group by Voucher (Consolidated)",
},
{
label: __("Group by Account"),
value: "Group by Account",
},
{
label: __("Group by Party"),
value: "Group by Party",
},
],
"default": "Group by Voucher (Consolidated)"
},
{
"fieldname":"tax_id",

View File

@ -62,14 +62,14 @@ def validate_filters(filters, account_details):
if not account_details.get(account):
frappe.throw(_("Account {0} does not exists").format(account))
if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
if (filters.get("account") and filters.get("group_by") == 'Group by Account'):
filters.account = frappe.parse_json(filters.get('account'))
for account in filters.account:
if account_details[account].is_group == 0:
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
if (filters.get("voucher_no")
and filters.get("group_by") in [_('Group by Voucher')]):
and filters.get("group_by") in ['Group by Voucher']):
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
if filters.from_date > filters.to_date:
@ -153,7 +153,7 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("include_dimensions"):
order_by_statement = "order by posting_date, creation"
if filters.get("group_by") == _("Group by Voucher"):
if filters.get("group_by") == "Group by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no"
if filters.get("include_default_book_entries"):
@ -312,13 +312,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
# Opening for filtered account
data.append(totals.opening)
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
for acc, acc_dict in iteritems(gle_map):
# acc
if acc_dict.entries:
# opening
data.append({})
if filters.get("group_by") != _("Group by Voucher"):
if filters.get("group_by") != "Group by Voucher":
data.append(acc_dict.totals.opening)
data += acc_dict.entries
@ -327,7 +327,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
data.append(acc_dict.totals.total)
# closing
if filters.get("group_by") != _("Group by Voucher"):
if filters.get("group_by") != "Group by Voucher":
data.append(acc_dict.totals.closing)
data.append({})
else:
@ -357,9 +357,9 @@ def get_totals_dict():
)
def group_by_field(group_by):
if group_by == _('Group by Party'):
if group_by == 'Group by Party':
return 'party'
elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]:
elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']:
return 'account'
else:
return 'voucher_no'
@ -423,9 +423,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
elif gle.posting_date <= to_date:
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == _('Group by Voucher (Consolidated)'):
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
for dim in accounting_dimensions:
keylist.append(gle.get(dim))

View File

@ -1,12 +1,15 @@
{
"add_total_row": 0,
"add_total_row": 1,
"columns": [],
"creation": "2018-08-21 11:25:00.551823",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2018-09-21 11:25:00.551823",
"modified": "2021-09-20 17:43:39.518851",
"modified_by": "Administrator",
"module": "Accounts",
"name": "TDS Computation Summary",

View File

@ -2,11 +2,10 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import flt
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
get_advance_vouchers,
get_debit_note_amount,
from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
get_result,
get_tds_docs,
)
from erpnext.accounts.utils import get_fiscal_year
@ -17,9 +16,12 @@ def execute(filters=None):
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
columns = get_columns(filters)
res = get_result(filters)
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
return columns, res
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
final_result = group_by_supplier_and_category(res)
return columns, final_result
def validate_filters(filters):
''' Validate if dates are properly set and lie in the same fiscal year'''
@ -33,81 +35,39 @@ def validate_filters(filters):
filters["fiscal_year"] = from_year
def get_result(filters):
# if no supplier selected, fetch data for all tds applicable supplier
# else fetch relevant data for selected supplier
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
fields = ["name", pan+" as pan", "tax_withholding_category", "supplier_type", "supplier_name"]
def group_by_supplier_and_category(data):
supplier_category_wise_map = {}
if filters.supplier:
filters.supplier = frappe.db.get_list('Supplier',
{"name": filters.supplier}, fields)
else:
filters.supplier = frappe.db.get_list('Supplier',
{"tax_withholding_category": ["!=", ""]}, fields)
for row in data:
supplier_category_wise_map.setdefault((row.get('supplier'), row.get('section_code')), {
'pan': row.get('pan'),
'supplier': row.get('supplier'),
'supplier_name': row.get('supplier_name'),
'section_code': row.get('section_code'),
'entity_type': row.get('entity_type'),
'tds_rate': row.get('tds_rate'),
'total_amount_credited': 0.0,
'tds_deducted': 0.0
})
supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['total_amount_credited'] += \
row.get('total_amount_credited', 0.0)
supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['tds_deducted'] += \
row.get('tds_deducted', 0.0)
final_result = get_final_result(supplier_category_wise_map)
return final_result
def get_final_result(supplier_category_wise_map):
out = []
for supplier in filters.supplier:
tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category)
rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year]
if rate:
rate = rate[0]
try:
account = [d.account for d in tds.accounts if d.company == filters.company][0]
except IndexError:
account = []
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
if total_invoiced_amount or tds_deducted:
row = [supplier.pan, supplier.name]
if filters.naming_series == 'Naming Series':
row.append(supplier.supplier_name)
row.extend([tds.name, supplier.supplier_type, rate, total_invoiced_amount, tds_deducted])
out.append(row)
for key, value in supplier_category_wise_map.items():
out.append(value)
return out
def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
''' calculate total invoice amount and total tds deducted for given supplier '''
entries = frappe.db.sql("""
select voucher_no, credit
from `tabGL Entry`
where party in (%s) and credit > 0
and company=%s and is_cancelled = 0
and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1)
supplier_credit_amount = flt(sum(d.credit for d in entries))
vouchers = [d.voucher_no for d in entries]
vouchers += get_advance_vouchers([supplier], company=company,
from_date=from_date, to_date=to_date)
tds_deducted = 0
if vouchers:
tds_deducted = flt(frappe.db.sql("""
select sum(credit)
from `tabGL Entry`
where account=%s and posting_date between %s and %s
and company=%s and credit > 0 and voucher_no in ({0})
""".format(', '.join("'%s'" % d for d in vouchers)),
(account, from_date, to_date, company))[0][0])
date_range_filter = [fiscal_year, from_date, to_date]
debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
return total_invoiced_amount, tds_deducted
def get_columns(filters):
columns = [
{
@ -149,7 +109,7 @@ def get_columns(filters):
{
"label": _("TDS Rate %"),
"fieldname": "tds_rate",
"fieldtype": "Float",
"fieldtype": "Percent",
"width": 90
},
{

View File

@ -16,69 +16,6 @@ frappe.query_reports["TDS Payable Monthly"] = {
"label": __("Supplier"),
"fieldtype": "Link",
"options": "Supplier",
"get_query": function() {
return {
"filters": {
"tax_withholding_category": ["!=", ""],
}
}
},
on_change: function() {
frappe.query_report.set_filter_value("purchase_invoice", "");
frappe.query_report.refresh();
}
},
{
"fieldname":"purchase_invoice",
"label": __("Purchase Invoice"),
"fieldtype": "Link",
"options": "Purchase Invoice",
"get_query": function() {
return {
"filters": {
"name": ["in", frappe.query_report.invoices]
}
}
},
on_change: function() {
let supplier = frappe.query_report.get_filter_value('supplier');
if(!supplier) return; // return if no supplier selected
// filter invoices based on selected supplier
let invoices = [];
frappe.query_report.invoice_data.map(d => {
if(d.supplier==supplier)
invoices.push(d.name)
});
frappe.query_report.invoices = invoices;
frappe.query_report.refresh();
}
},
{
"fieldname":"purchase_order",
"label": __("Purchase Order"),
"fieldtype": "Link",
"options": "Purchase Order",
"get_query": function() {
return {
"filters": {
"name": ["in", frappe.query_report.invoices]
}
}
},
on_change: function() {
let supplier = frappe.query_report.get_filter_value('supplier');
if(!supplier) return; // return if no supplier selected
// filter invoices based on selected supplier
let invoices = [];
frappe.query_report.invoice_data.map(d => {
if(d.supplier==supplier)
invoices.push(d.name)
});
frappe.query_report.invoices = invoices;
frappe.query_report.refresh();
}
},
{
"fieldname":"from_date",
@ -96,23 +33,5 @@ frappe.query_reports["TDS Payable Monthly"] = {
"reqd": 1,
"width": "60px"
}
],
onload: function(report) {
// fetch all tds applied invoices
frappe.call({
"method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
callback: function(r) {
let invoices = [];
r.message.map(d => {
invoices.push(d.name);
});
report["invoice_data"] = r.message.invoices;
report["invoices"] = invoices;
}
});
}
]
}

View File

@ -1,13 +1,15 @@
{
"add_total_row": 1,
"columns": [],
"creation": "2018-08-21 11:32:30.874923",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2019-09-24 13:46:16.473711",
"modified": "2021-09-20 12:05:50.387572",
"modified_by": "Administrator",
"module": "Accounts",
"name": "TDS Payable Monthly",

View File

@ -8,19 +8,12 @@ from frappe import _
def execute(filters=None):
filters["invoices"] = frappe.cache().hget("invoices", frappe.session.user)
validate_filters(filters)
set_filters(filters)
# TDS payment entries
payment_entries = get_payment_entires(filters)
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
columns = get_columns(filters)
if not filters.get("invoices"):
return columns, []
res = get_result(filters, payment_entries)
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
return columns, res
def validate_filters(filters):
@ -28,109 +21,59 @@ def validate_filters(filters):
if filters.from_date > filters.to_date:
frappe.throw(_("From Date must be before To Date"))
def set_filters(filters):
invoices = []
if not filters.get("invoices"):
filters["invoices"] = get_tds_invoices_and_orders()
if filters.supplier and filters.purchase_invoice:
for d in filters["invoices"]:
if d.name == filters.purchase_invoice and d.supplier == filters.supplier:
invoices.append(d)
elif filters.supplier and not filters.purchase_invoice:
for d in filters["invoices"]:
if d.supplier == filters.supplier:
invoices.append(d)
elif filters.purchase_invoice and not filters.supplier:
for d in filters["invoices"]:
if d.name == filters.purchase_invoice:
invoices.append(d)
elif filters.supplier and filters.purchase_order:
for d in filters.get("invoices"):
if d.name == filters.purchase_order and d.supplier == filters.supplier:
invoices.append(d)
elif filters.supplier and not filters.purchase_order:
for d in filters.get("invoices"):
if d.supplier == filters.supplier:
invoices.append(d)
elif filters.purchase_order and not filters.supplier:
for d in filters.get("invoices"):
if d.name == filters.purchase_order:
invoices.append(d)
filters["invoices"] = invoices if invoices else filters["invoices"]
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
#print(filters.get('invoices'))
def get_result(filters, payment_entries):
supplier_map, tds_docs = get_supplier_map(filters, payment_entries)
documents = [d.get('name') for d in filters.get('invoices')] + [d.get('name') for d in payment_entries]
gle_map = get_gle_map(filters, documents)
def get_result(filters, tds_docs, tds_accounts, tax_category_map):
supplier_map = get_supplier_pan_map()
tax_rate_map = get_tax_rate_map(filters)
gle_map = get_gle_map(filters, tds_docs)
out = []
for d in gle_map:
for name, details in gle_map.items():
tds_deducted, total_amount_credited = 0, 0
supplier = supplier_map[d]
tax_withholding_category = tax_category_map.get(name)
rate = tax_rate_map.get(tax_withholding_category)
tds_doc = tds_docs[supplier.tax_withholding_category]
account_list = [i.account for i in tds_doc.accounts if i.company == filters.company]
for entry in details:
supplier = entry.party or entry.against
posting_date = entry.posting_date
voucher_type = entry.voucher_type
if account_list:
account = account_list[0]
if entry.account in tds_accounts:
tds_deducted += (entry.credit - entry.debit)
for k in gle_map[d]:
if k.party == supplier_map[d] and k.credit > 0:
total_amount_credited += (k.credit - k.debit)
elif account_list and k.account == account and (k.credit - k.debit) > 0:
tds_deducted = (k.credit - k.debit)
total_amount_credited += (k.credit - k.debit)
voucher_type = k.voucher_type
total_amount_credited += (entry.credit - entry.debit)
rate = [i.tax_withholding_rate for i in tds_doc.rates
if i.fiscal_year == gle_map[d][0].fiscal_year]
if rate and len(rate) > 0 and tds_deducted:
rate = rate[0]
row = [supplier.pan, supplier.name]
if rate and tds_deducted:
row = {
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
'supplier': supplier_map.get(supplier).name
}
if filters.naming_series == 'Naming Series':
row.append(supplier.supplier_name)
row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
row.update({
'section_code': tax_withholding_category,
'entity_type': supplier_map.get(supplier).supplier_type,
'tds_rate': rate,
'total_amount_credited': total_amount_credited,
'tds_deducted': tds_deducted,
'transaction_date': posting_date,
'transaction_type': voucher_type,
'ref_no': name
})
row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
tds_deducted, gle_map[d][0].posting_date, voucher_type, d])
out.append(row)
return out
def get_supplier_map(filters, payment_entries):
# create a supplier_map of the form {"purchase_invoice": {supplier_name, pan, tds_name}}
# pre-fetch all distinct applicable tds docs
supplier_map, tds_docs = {}, {}
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
supplier_list = [d.supplier for d in filters["invoices"]]
def get_supplier_pan_map():
supplier_map = frappe._dict()
suppliers = frappe.db.get_all('Supplier', fields=['name', 'pan', 'supplier_type', 'supplier_name'])
supplier_detail = frappe.db.get_all('Supplier',
{"name": ["in", supplier_list]},
["tax_withholding_category", "name", pan+" as pan", "supplier_type", "supplier_name"])
for d in suppliers:
supplier_map[d.name] = d
for d in filters["invoices"]:
supplier_map[d.get("name")] = [k for k in supplier_detail
if k.name == d.get("supplier")][0]
for d in payment_entries:
supplier_map[d.get("name")] = [k for k in supplier_detail
if k.name == d.get("supplier")][0]
for d in supplier_detail:
if d.get("tax_withholding_category") not in tds_docs:
tds_docs[d.get("tax_withholding_category")] = \
frappe.get_doc("Tax Withholding Category", d.get("tax_withholding_category"))
return supplier_map, tds_docs
return supplier_map
def get_gle_map(filters, documents):
# create gle_map of the form
@ -140,10 +83,9 @@ def get_gle_map(filters, documents):
gle = frappe.db.get_all('GL Entry',
{
"voucher_no": ["in", documents],
'is_cancelled': 0,
'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
"credit": (">", 0)
},
["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date", "voucher_type"],
["credit", "debit", "account", "voucher_no", "posting_date", "voucher_type", "against", "party"],
)
for d in gle:
@ -233,39 +175,57 @@ def get_columns(filters):
return columns
def get_payment_entires(filters):
filter_dict = {
'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
'party_type': 'Supplier',
'apply_tax_withholding_amount': 1
def get_tds_docs(filters):
tds_documents = []
purchase_invoices = []
payment_entries = []
journal_entries = []
tax_category_map = {}
tds_accounts = frappe.get_all("Tax Withholding Account", {'company': filters.get('company')},
pluck="account")
query_filters = {
"credit": ('>', 0),
"account": ("in", tds_accounts),
"posting_date": ("between", [filters.get("from_date"), filters.get("to_date")]),
"is_cancelled": 0
}
if filters.get('purchase_invoice') or filters.get('purchase_order'):
parent = frappe.db.get_all('Payment Entry Reference',
{'reference_name': ('in', [d.get('name') for d in filters.get('invoices')])}, ['parent'])
if filters.get('supplier'):
query_filters.update({'against': filters.get('supplier')})
filter_dict.update({'name': ('in', [d.get('parent') for d in parent])})
tds_docs = frappe.get_all("GL Entry", query_filters, ["voucher_no", "voucher_type", "against", "party"])
payment_entries = frappe.get_all('Payment Entry', fields=['name', 'party_name as supplier'],
filters=filter_dict)
for d in tds_docs:
if d.voucher_type == "Purchase Invoice":
purchase_invoices.append(d.voucher_no)
elif d.voucher_type == "Payment Entry":
payment_entries.append(d.voucher_no)
elif d.voucher_type == "Journal Entry":
journal_entries.append(d.voucher_no)
return payment_entries
tds_documents.append(d.voucher_no)
@frappe.whitelist()
def get_tds_invoices_and_orders():
# fetch tds applicable supplier and fetch invoices for these suppliers
suppliers = [d.name for d in frappe.db.get_list("Supplier",
{"tax_withholding_category": ["!=", ""]}, ["name"])]
if purchase_invoices:
get_tax_category_map(purchase_invoices, 'Purchase Invoice', tax_category_map)
invoices = frappe.db.get_list("Purchase Invoice",
{"supplier": ["in", suppliers]}, ["name", "supplier"])
if payment_entries:
get_tax_category_map(payment_entries, 'Payment Entry', tax_category_map)
orders = frappe.db.get_list("Purchase Order",
{"supplier": ["in", suppliers]}, ["name", "supplier"])
if journal_entries:
get_tax_category_map(journal_entries, 'Journal Entry', tax_category_map)
invoices = invoices + orders
invoices = [d for d in invoices if d.supplier]
return tds_documents, tds_accounts, tax_category_map
frappe.cache().hset("invoices", frappe.session.user, invoices)
def get_tax_category_map(vouchers, doctype, tax_category_map):
tax_category_map.update(frappe._dict(frappe.get_all(doctype,
filters = {'name': ('in', vouchers)}, fields=['name', 'tax_withholding_category'], as_list=1)))
return invoices
def get_tax_rate_map(filters):
rate_map = frappe.get_all('Tax Withholding Rate', filters={
'from_date': ('<=', filters.get('from_date')),
'to_date': ('>=', filters.get('to_date'))
}, fields=['parent', 'tax_withholding_rate'], as_list=1)
return frappe._dict(rate_map)

View File

@ -4,9 +4,10 @@
frappe.query_reports["Unpaid Expense Claim"] = {
"filters": [
{
"fieldname":"employee",
"fieldname": "employee",
"label": __("Employee"),
"fieldtype": "Link"
"fieldtype": "Link",
"options": "Employee"
}
]
}

View File

@ -5,21 +5,48 @@ import unittest
from frappe.test_runner import make_test_objects
from erpnext.accounts.party import get_party_shipping_address
from erpnext.accounts.utils import get_future_stock_vouchers, get_voucherwise_gl_entries
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
class TestUtils(unittest.TestCase):
@classmethod
def setUpClass(cls):
super(TestUtils, cls).setUpClass()
make_test_objects('Address', ADDRESS_RECORDS)
make_test_objects("Address", ADDRESS_RECORDS)
def test_get_party_shipping_address(self):
address = get_party_shipping_address('Customer', '_Test Customer 1')
self.assertEqual(address, '_Test Billing Address 2 Title-Billing')
address = get_party_shipping_address("Customer", "_Test Customer 1")
self.assertEqual(address, "_Test Billing Address 2 Title-Billing")
def test_get_party_shipping_address2(self):
address = get_party_shipping_address('Customer', '_Test Customer 2')
self.assertEqual(address, '_Test Shipping Address 2 Title-Shipping')
address = get_party_shipping_address("Customer", "_Test Customer 2")
self.assertEqual(address, "_Test Shipping Address 2 Title-Shipping")
def test_get_voucher_wise_gl_entry(self):
pr = make_purchase_receipt(
item_code="_Test Item",
posting_date="2021-02-01",
rate=100,
qty=1,
warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory",
)
future_vouchers = get_future_stock_vouchers("2021-01-01", "00:00:00", for_items=["_Test Item"])
voucher_type_and_no = ("Purchase Receipt", pr.name)
self.assertTrue(
voucher_type_and_no in future_vouchers,
msg="get_future_stock_vouchers not returning correct value",
)
posting_date = "2021-01-01"
gl_entries = get_voucherwise_gl_entries(future_vouchers, posting_date)
self.assertTrue(
voucher_type_and_no in gl_entries, msg="get_voucherwise_gl_entries not returning expected GLes",
)
ADDRESS_RECORDS = [
@ -31,12 +58,8 @@ ADDRESS_RECORDS = [
"city": "Lagos",
"country": "Nigeria",
"links": [
{
"link_doctype": "Customer",
"link_name": "_Test Customer 2",
"doctype": "Dynamic Link"
}
]
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
],
},
{
"doctype": "Address",
@ -46,12 +69,8 @@ ADDRESS_RECORDS = [
"city": "Lagos",
"country": "Nigeria",
"links": [
{
"link_doctype": "Customer",
"link_name": "_Test Customer 2",
"doctype": "Dynamic Link"
}
]
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
],
},
{
"doctype": "Address",
@ -62,12 +81,8 @@ ADDRESS_RECORDS = [
"country": "Nigeria",
"is_shipping_address": "1",
"links": [
{
"link_doctype": "Customer",
"link_name": "_Test Customer 2",
"doctype": "Dynamic Link"
}
]
{"link_doctype": "Customer", "link_name": "_Test Customer 2", "doctype": "Dynamic Link"}
],
},
{
"doctype": "Address",
@ -78,11 +93,7 @@ ADDRESS_RECORDS = [
"country": "Nigeria",
"is_shipping_address": "1",
"links": [
{
"link_doctype": "Customer",
"link_name": "_Test Customer 1",
"doctype": "Dynamic Link"
}
]
}
{"link_doctype": "Customer", "link_name": "_Test Customer 1", "doctype": "Dynamic Link"}
],
},
]

View File

@ -963,6 +963,9 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
Only fetches GLE fields required for comparing with new GLE.
Check compare_existing_and_expected_gle function below.
returns:
Dict[Tuple[voucher_type, voucher_no], List[GL Entries]]
"""
gl_entries = {}
if not future_stock_vouchers:
@ -971,7 +974,7 @@ def get_voucherwise_gl_entries(future_stock_vouchers, posting_date):
voucher_nos = [d[1] for d in future_stock_vouchers]
gles = frappe.db.sql("""
select name, account, credit, debit, cost_center, project
select name, account, credit, debit, cost_center, project, voucher_type, voucher_no
from `tabGL Entry`
where
posting_date >= %s and voucher_no in (%s)""" %

View File

@ -394,10 +394,6 @@ class Asset(AccountsController):
if cint(self.number_of_depreciations_booked) > cint(row.total_number_of_depreciations):
frappe.throw(_("Number of Depreciations Booked cannot be greater than Total Number of Depreciations"))
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(nowdate()):
frappe.msgprint(_("Depreciation Row {0}: Depreciation Start Date is entered as past date")
.format(row.idx), title=_('Warning'), indicator='red')
if row.depreciation_start_date and getdate(row.depreciation_start_date) < getdate(self.purchase_date):
frappe.throw(_("Depreciation Row {0}: Next Depreciation Date cannot be before Purchase Date")
.format(row.idx))

View File

@ -645,12 +645,18 @@ class TestAsset(unittest.TestCase):
pr = make_purchase_receipt(item_code="Macbook Pro",
qty=1, rate=8000.0, location="Test Location")
finance_book = frappe.new_doc('Finance Book')
finance_book.finance_book_name = 'Income Tax'
finance_book.for_income_tax = 1
finance_book.insert(ignore_if_duplicate=1)
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-07-12'
asset.purchase_date = '2030-01-01'
asset.append("finance_books", {
"finance_book": finance_book.name,
"expected_value_after_useful_life": 1000,
"depreciation_method": "Written Down Value",
"total_number_of_depreciations": 3,

View File

@ -28,7 +28,7 @@
"fieldname": "supp_master_name",
"fieldtype": "Select",
"label": "Supplier Naming By",
"options": "Supplier Name\nNaming Series"
"options": "Supplier Name\nNaming Series\nAuto Name"
},
{
"fieldname": "supplier_group",
@ -123,7 +123,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-06-24 10:38:28.934525",
"modified": "2021-09-08 19:26:23.548837",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",

View File

@ -425,7 +425,10 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
status: ["!=", "Stopped"],
per_ordered: ["<", 100],
company: me.frm.doc.company
}
},
allow_child_item_selection: true,
child_fielname: "items",
child_columns: ["item_code", "qty"]
})
}, __("Get Items From"));

View File

@ -1121,6 +1121,7 @@
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@ -1143,7 +1144,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2021-08-30 20:03:14.008804",
"modified": "2021-09-28 13:10:47.955401",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",

View File

@ -394,12 +394,10 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
@frappe.whitelist()
def get_supplier_tag():
if not frappe.cache().hget("Supplier", "Tags"):
filters = {"document_type": "Supplier"}
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
frappe.cache().hset("Supplier", "Tags", tags)
filters = {"document_type": "Supplier"}
tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
return frappe.cache().hget("Supplier", "Tags")
return tags
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs

View File

@ -433,12 +433,12 @@
"image_field": "image",
"links": [
{
"group": "Item Group",
"link_doctype": "Supplier Item Group",
"link_fieldname": "supplier"
"group": "Allowed Items",
"link_doctype": "Party Specific Item",
"link_fieldname": "party"
}
],
"modified": "2021-08-27 18:02:44.314077",
"modified": "2021-09-06 17:37:56.522233",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",

View File

@ -10,7 +10,7 @@ from frappe.contacts.address_and_contact import (
delete_contact_and_address,
load_address_and_contact,
)
from frappe.model.naming import set_name_by_naming_series
from frappe.model.naming import set_name_by_naming_series, set_name_from_naming_options
from erpnext.accounts.party import get_dashboard_info, validate_party_accounts
from erpnext.utilities.transaction_base import TransactionBase
@ -40,8 +40,10 @@ class Supplier(TransactionBase):
supp_master_name = frappe.defaults.get_global_default('supp_master_name')
if supp_master_name == 'Supplier Name':
self.name = self.supplier_name
else:
elif supp_master_name == 'Naming Series':
set_name_by_naming_series(self)
else:
self.name = set_name_from_naming_options(frappe.get_meta(self.doctype).autoname, self)
def on_update(self):
if not self.naming_series:

View File

@ -1,77 +0,0 @@
{
"actions": [],
"creation": "2021-05-07 18:16:40.621421",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"supplier",
"item_group"
],
"fields": [
{
"fieldname": "supplier",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Supplier",
"options": "Supplier",
"reqd": 1
},
{
"fieldname": "item_group",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Group",
"options": "Item Group",
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2021-05-19 13:48:16.742303",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Item Group",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase User",
"share": 1,
"write": 1
},
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Purchase Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -1,20 +0,0 @@
# -*- 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 import _
from frappe.model.document import Document
class SupplierItemGroup(Document):
def validate(self):
exists = frappe.db.exists({
'doctype': 'Supplier Item Group',
'supplier': self.supplier,
'item_group': self.item_group
})
if exists:
frappe.throw(_("Item Group has already been linked to this supplier."))

View File

@ -1,11 +0,0 @@
# -*- 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 TestSupplierItemGroup(unittest.TestCase):
pass

View File

@ -30,7 +30,14 @@ frappe.query_reports["Purchase Order Analysis"] = {
"default": frappe.datetime.get_today()
},
{
"fieldname": "purchase_order",
"fieldname":"project",
"label": __("Project"),
"fieldtype": "Link",
"width": "80",
"options": "Project"
},
{
"fieldname": "name",
"label": __("Purchase Order"),
"fieldtype": "Link",
"width": "80",

View File

@ -41,14 +41,12 @@ def get_conditions(filters):
if filters.get("from_date") and filters.get("to_date"):
conditions += " and po.transaction_date between %(from_date)s and %(to_date)s"
if filters.get("company"):
conditions += " and po.company = %(company)s"
for field in ['company', 'name', 'status']:
if filters.get(field):
conditions += f" and po.{field} = %({field})s"
if filters.get("purchase_order"):
conditions += " and po.name = %(purchase_order)s"
if filters.get("status"):
conditions += " and po.status in %(status)s"
if filters.get('project'):
conditions += " and poi.project = %(project)s"
return conditions
@ -57,6 +55,7 @@ def get_data(conditions, filters):
SELECT
po.transaction_date as date,
poi.schedule_date as required_date,
poi.project,
po.name as purchase_order,
po.status, po.supplier, poi.item_code,
poi.qty, poi.received_qty,
@ -175,6 +174,12 @@ def get_columns(filters):
"fieldtype": "Link",
"options": "Supplier",
"width": 130
},{
"label": _("Project"),
"fieldname": "project",
"fieldtype": "Link",
"options": "Project",
"width": 130
}]
if not filters.get("group_by_po"):

View File

@ -685,13 +685,17 @@ class AccountsController(TransactionBase):
.format(d.reference_name, d.against_order))
def set_advance_gain_or_loss(self):
if not self.get("advances"):
if self.get('conversion_rate') == 1 or not self.get("advances"):
return
is_purchase_invoice = self.doctype == 'Purchase Invoice'
party_account = self.credit_to if is_purchase_invoice else self.debit_to
if get_account_currency(party_account) != self.currency:
return
for d in self.get("advances"):
advance_exchange_rate = d.ref_exchange_rate
if (d.allocated_amount and self.conversion_rate != 1
and self.conversion_rate != advance_exchange_rate):
if (d.allocated_amount and self.conversion_rate != advance_exchange_rate):
base_allocated_amount_in_ref_rate = advance_exchange_rate * d.allocated_amount
base_allocated_amount_in_inv_rate = self.conversion_rate * d.allocated_amount
@ -710,7 +714,7 @@ class AccountsController(TransactionBase):
gain_loss_account = frappe.db.get_value('Company', self.company, 'exchange_gain_loss_account')
if not gain_loss_account:
frappe.throw(_("Please set Default Exchange Gain/Loss Account in Company {}")
frappe.throw(_("Please set default Exchange Gain/Loss Account in Company {}")
.format(self.get('company')))
account_currency = get_account_currency(gain_loss_account)
if account_currency != self.company_currency:
@ -729,7 +733,7 @@ class AccountsController(TransactionBase):
"against": party,
dr_or_cr + "_in_account_currency": abs(d.exchange_gain_loss),
dr_or_cr: abs(d.exchange_gain_loss),
"cost_center": self.cost_center,
"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company),
"project": self.project
}, item=d)
)
@ -980,42 +984,55 @@ class AccountsController(TransactionBase):
item_allowance = {}
global_qty_allowance, global_amount_allowance = None, None
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
user_roles = frappe.get_roles()
total_overbilled_amt = 0.0
for item in self.get("items"):
if item.get(item_ref_dn):
ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
item.get(item_ref_dn), based_on), self.precision(based_on, item))
if not ref_amt:
frappe.msgprint(
_("Warning: System will not check overbilling since amount for Item {0} in {1} is zero")
.format(item.item_code, ref_dt))
else:
already_billed = frappe.db.sql("""
select sum(%s)
from `tab%s`
where %s=%s and docstatus=1 and parent != %s
""" % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'),
(item.get(item_ref_dn), self.name))[0][0]
if not item.get(item_ref_dn):
continue
total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)),
self.precision(based_on, item))
ref_amt = flt(frappe.db.get_value(ref_dt + " Item",
item.get(item_ref_dn), based_on), self.precision(based_on, item))
if not ref_amt:
frappe.msgprint(
_("System will not check overbilling since amount for Item {0} in {1} is zero")
.format(item.item_code, ref_dt), title=_("Warning"), indicator="orange")
continue
allowance, item_allowance, global_qty_allowance, global_amount_allowance = \
get_allowance_for(item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount")
already_billed = frappe.db.sql("""
select sum(%s)
from `tab%s`
where %s=%s and docstatus=1 and parent != %s
""" % (based_on, self.doctype + " Item", item_ref_dn, '%s', '%s'),
(item.get(item_ref_dn), self.name))[0][0]
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
total_billed_amt = flt(flt(already_billed) + flt(item.get(based_on)),
self.precision(based_on, item))
if total_billed_amt < 0 and max_allowed_amt < 0:
# while making debit note against purchase return entry(purchase receipt) getting overbill error
total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_amt)
allowance, item_allowance, global_qty_allowance, global_amount_allowance = \
get_allowance_for(item.item_code, item_allowance, global_qty_allowance, global_amount_allowance, "amount")
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
max_allowed_amt = flt(ref_amt * (100 + allowance) / 100)
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
if self.doctype != "Purchase Invoice":
self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt)
if total_billed_amt < 0 and max_allowed_amt < 0:
# while making debit note against purchase return entry(purchase receipt) getting overbill error
total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_amt)
overbill_amt = total_billed_amt - max_allowed_amt
total_overbilled_amt += overbill_amt
if overbill_amt > 0.01 and role_allowed_to_over_bill not in user_roles:
if self.doctype != "Purchase Invoice":
self.throw_overbill_exception(item, max_allowed_amt)
elif not cint(frappe.db.get_single_value("Buying Settings", "bill_for_rejected_quantity_in_purchase_invoice")):
self.throw_overbill_exception(item, max_allowed_amt)
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
@ -1668,14 +1685,18 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
return list(payment_entries_against_order) + list(unallocated_payment_entries)
def update_invoice_status():
# Daily update the status of the invoices
frappe.db.sql(""" update `tabSales Invoice` set status = 'Overdue'
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
frappe.db.sql(""" update `tabPurchase Invoice` set status = 'Overdue'
where due_date < CURDATE() and docstatus = 1 and outstanding_amount > 0""")
"""Updates status as Overdue for applicable invoices. Runs daily."""
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
update `tab{}` as dt set dt.status = 'Overdue'
where dt.docstatus = 1
and dt.status != 'Overdue'
and dt.outstanding_amount > 0
and (dt.grand_total - dt.outstanding_amount) <
(select sum(payment_amount) from `tabPayment Schedule` as ps
where ps.parent = dt.name and ps.due_date < %s)
""".format(doctype), getdate())
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):

View File

@ -7,6 +7,7 @@ import json
from collections import defaultdict
import frappe
from frappe import scrub
from frappe.desk.reportview import get_filters_cond, get_match_cond
from frappe.utils import nowdate, unique
@ -223,18 +224,29 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
if filters and isinstance(filters, dict) and filters.get('supplier'):
item_group_list = frappe.get_all('Supplier Item Group',
filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
if filters and isinstance(filters, dict):
if filters.get('customer') or filters.get('supplier'):
party = filters.get('customer') or filters.get('supplier')
item_rules_list = frappe.get_all('Party Specific Item',
filters = {'party': party}, fields = ['restrict_based_on', 'based_on_value'])
item_groups = []
for i in item_group_list:
item_groups.append(i.item_group)
filters_dict = {}
for rule in item_rules_list:
if rule['restrict_based_on'] == 'Item':
rule['restrict_based_on'] = 'name'
filters_dict[rule.restrict_based_on] = []
del filters['supplier']
for rule in item_rules_list:
filters_dict[rule.restrict_based_on].append(rule.based_on_value)
for filter in filters_dict:
filters[scrub(filter)] = ['in', filters_dict[filter]]
if filters.get('customer'):
del filters['customer']
else:
del filters['supplier']
if item_groups:
filters['item_group'] = ['in', item_groups]
description_cond = ''
if frappe.db.count('Item', cache=True) < 50000:
@ -307,7 +319,7 @@ def bom(doctype, txt, searchfield, start, page_len, filters):
@frappe.validate_and_sanitize_search_inputs
def get_project_name(doctype, txt, searchfield, start, page_len, filters):
cond = ''
if filters.get('customer'):
if filters and filters.get('customer'):
cond = """(`tabProject`.customer = %s or
ifnull(`tabProject`.customer,"")="") and""" %(frappe.db.escape(filters.get("customer")))

View File

@ -0,0 +1,87 @@
import unittest
from functools import partial
from erpnext.controllers import queries
def add_default_params(func, doctype):
return partial(
func, doctype=doctype, txt="", searchfield="name", start=0, page_len=20, filters=None
)
class TestQueries(unittest.TestCase):
# All tests are based on doctype/test_records.json
def assert_nested_in(self, item, container):
self.assertIn(item, [vals for tuples in container for vals in tuples])
def test_employee_query(self):
query = add_default_params(queries.employee_query, "Employee")
self.assertGreaterEqual(len(query(txt="_Test Employee")), 3)
self.assertGreaterEqual(len(query(txt="_Test Employee 1")), 1)
def test_lead_query(self):
query = add_default_params(queries.lead_query, "Lead")
self.assertGreaterEqual(len(query(txt="_Test Lead")), 4)
self.assertEqual(len(query(txt="_Test Lead 4")), 1)
def test_customer_query(self):
query = add_default_params(queries.customer_query, "Customer")
self.assertGreaterEqual(len(query(txt="_Test Customer")), 7)
self.assertGreaterEqual(len(query(txt="_Test Customer USD")), 1)
def test_supplier_query(self):
query = add_default_params(queries.supplier_query, "Supplier")
self.assertGreaterEqual(len(query(txt="_Test Supplier")), 7)
self.assertGreaterEqual(len(query(txt="_Test Supplier USD")), 1)
def test_item_query(self):
query = add_default_params(queries.item_query, "Item")
self.assertGreaterEqual(len(query(txt="_Test Item")), 7)
self.assertEqual(len(query(txt="_Test Item Home Desktop 100 3")), 1)
fg_item = "_Test FG Item"
stock_items = query(txt=fg_item, filters={"is_stock_item": 1})
self.assert_nested_in("_Test FG Item", stock_items)
bundled_stock_items = query(txt="_test product bundle item 5", filters={"is_stock_item": 1})
self.assertEqual(len(bundled_stock_items), 0)
def test_bom_qury(self):
query = add_default_params(queries.bom, "BOM")
self.assertGreaterEqual(len(query(txt="_Test Item Home Desktop Manufactured")), 1)
def test_project_query(self):
query = add_default_params(queries.get_project_name, "BOM")
self.assertGreaterEqual(len(query(txt="_Test Project")), 1)
def test_account_query(self):
query = add_default_params(queries.get_account_list, "Account")
debtor_accounts = query(txt="Debtors", filters={"company": "_Test Company"})
self.assert_nested_in("Debtors - _TC", debtor_accounts)
def test_income_account_query(self):
query = add_default_params(queries.get_income_account, "Account")
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
def test_expense_account_query(self):
query = add_default_params(queries.get_expense_account, "Account")
self.assertGreaterEqual(len(query(filters={"company": "_Test Company"})), 1)
def test_warehouse_query(self):
query = add_default_params(queries.warehouse_query, "Account")
wh = query(filters=[["Bin", "item_code", "=", "_Test Item"]])
self.assertGreaterEqual(len(wh), 1)

View File

@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.modules.utils import get_module_app
from frappe.utils import flt, has_common
from frappe.utils.user import is_website_user
@ -21,8 +22,32 @@ def get_list_context(context=None):
"get_list": get_transaction_list
}
def get_webform_list_context(module):
if get_module_app(module) != 'erpnext':
return
return {
"get_list": get_webform_transaction_list
}
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
def get_webform_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified"):
""" Get List of transactions for custom doctypes """
from frappe.www.list import get_list
if not filters:
filters = []
meta = frappe.get_meta(doctype)
for d in meta.fields:
if d.fieldtype == 'Link' and d.fieldname != 'amended_from':
allowed_docs = [d.name for d in get_transaction_list(doctype=d.options, custom=True)]
allowed_docs.append('')
filters.append((d.fieldname, 'in', allowed_docs))
return get_list(doctype, txt, filters, limit_start, limit_page_length, ignore_permissions=False,
fields=None, order_by="modified")
def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by="modified", custom=False):
user = frappe.session.user
ignore_permissions = False
@ -46,7 +71,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
filters.append(('customer', 'in', customers))
elif suppliers:
filters.append(('supplier', 'in', suppliers))
else:
elif not custom:
return []
if doctype == 'Request for Quotation':
@ -56,9 +81,16 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p
# Since customers and supplier do not have direct access to internal doctypes
ignore_permissions = True
if not customers and not suppliers and custom:
ignore_permissions = False
filters = []
transactions = get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length,
fields='name', ignore_permissions=ignore_permissions, order_by='modified desc')
if custom:
return transactions
return post_process(doctype, transactions)
def get_list_for_transactions(doctype, txt, filters, limit_start, limit_page_length=20,

View File

@ -132,10 +132,43 @@ frappe.ui.form.on("Opportunity", {
}
},
currency: function(frm) {
let company_currency = erpnext.get_currency(frm.doc.company);
if (company_currency != frm.doc.company) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
from_currency: frm.doc.currency,
to_currency: company_currency
},
callback: function(r) {
if (r.message) {
frm.set_value('conversion_rate', flt(r.message));
frm.set_df_property('conversion_rate', 'description', '1 ' + frm.doc.currency
+ ' = [?] ' + company_currency);
}
}
});
} else {
frm.set_value('conversion_rate', 1.0);
frm.set_df_property('conversion_rate', 'hidden', 1);
frm.set_df_property('conversion_rate', 'description', '');
}
frm.trigger('opportunity_amount');
frm.trigger('set_dynamic_field_label');
},
opportunity_amount: function(frm) {
frm.set_value('base_opportunity_amount', flt(frm.doc.opportunity_amount) * flt(frm.doc.conversion_rate));
},
set_dynamic_field_label: function(frm){
if (frm.doc.opportunity_from) {
frm.set_df_property("party_name", "label", frm.doc.opportunity_from);
}
frm.trigger('change_grid_labels');
frm.trigger('change_form_labels');
},
make_supplier_quotation: function(frm) {
@ -152,6 +185,62 @@ frappe.ui.form.on("Opportunity", {
})
},
change_form_labels: function(frm) {
let company_currency = erpnext.get_currency(frm.doc.company);
frm.set_currency_labels(["base_opportunity_amount", "base_total", "base_grand_total"], company_currency);
frm.set_currency_labels(["opportunity_amount", "total", "grand_total"], frm.doc.currency);
// toggle fields
frm.toggle_display(["conversion_rate", "base_opportunity_amount", "base_total", "base_grand_total"],
frm.doc.currency != company_currency);
},
change_grid_labels: function(frm) {
let company_currency = erpnext.get_currency(frm.doc.company);
frm.set_currency_labels(["base_rate", "base_amount"], company_currency, "items");
frm.set_currency_labels(["rate", "amount"], frm.doc.currency, "items");
let item_grid = frm.fields_dict.items.grid;
$.each(["base_rate", "base_amount"], function(i, fname) {
if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, frm.doc.currency != company_currency);
});
frm.refresh_fields();
},
calculate_total: function(frm) {
let total = 0, base_total = 0, grand_total = 0, base_grand_total = 0;
frm.doc.items.forEach(item => {
total += item.amount;
base_total += item.base_amount;
})
base_grand_total = base_total + frm.doc.base_opportunity_amount;
grand_total = total + frm.doc.opportunity_amount;
frm.set_value({
'total': flt(total),
'base_total': flt(base_total),
'grand_total': flt(grand_total),
'base_grand_total': flt(base_grand_total)
});
}
});
frappe.ui.form.on("Opportunity Item", {
calculate: function(frm, cdt, cdn) {
let row = frappe.get_doc(cdt, cdn);
frappe.model.set_value(cdt, cdn, "amount", flt(row.qty) * flt(row.rate));
frappe.model.set_value(cdt, cdn, "base_rate", flt(frm.doc.conversion_rate) * flt(row.rate));
frappe.model.set_value(cdt, cdn, "base_amount", flt(frm.doc.conversion_rate) * flt(row.amount));
frm.trigger("calculate_total");
},
qty: function(frm, cdt, cdn) {
frm.trigger("calculate", cdt, cdn);
},
rate: function(frm, cdt, cdn) {
frm.trigger("calculate", cdt, cdn);
}
})
// TODO commonify this code
@ -169,6 +258,7 @@ erpnext.crm.Opportunity = class Opportunity extends frappe.ui.form.Controller {
}
this.setup_queries();
this.frm.trigger('currency');
}
setup_queries() {

View File

@ -33,12 +33,20 @@
"to_discuss",
"section_break_14",
"currency",
"opportunity_amount",
"conversion_rate",
"base_opportunity_amount",
"with_items",
"column_break_17",
"probability",
"opportunity_amount",
"items_section",
"items",
"section_break_32",
"base_total",
"base_grand_total",
"column_break_33",
"total",
"grand_total",
"contact_info",
"customer_address",
"address_display",
@ -425,12 +433,65 @@
"fieldtype": "Link",
"label": "Print Language",
"options": "Language"
},
{
"fieldname": "base_opportunity_amount",
"fieldtype": "Currency",
"label": "Opportunity Amount (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"depends_on": "with_items",
"fieldname": "section_break_32",
"fieldtype": "Section Break",
"hide_border": 1
},
{
"fieldname": "base_total",
"fieldtype": "Currency",
"label": "Total (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
"read_only": 1
},
{
"fieldname": "conversion_rate",
"fieldtype": "Float",
"label": "Exchange Rate"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "base_grand_total",
"fieldtype": "Currency",
"label": "Grand Total (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "grand_total",
"fieldtype": "Currency",
"label": "Grand Total",
"options": "currency",
"read_only": 1
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
"modified": "2021-08-25 10:28:24.923543",
"modified": "2021-09-06 10:02:18.609136",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",

View File

@ -9,9 +9,8 @@ import frappe
from frappe import _
from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import cint, cstr, get_fullname
from frappe.utils import cint, cstr, flt, get_fullname
from erpnext.accounts.party import get_party_account_currency
from erpnext.setup.utils import get_exchange_rate
from erpnext.utilities.transaction_base import TransactionBase
@ -41,6 +40,23 @@ class Opportunity(TransactionBase):
if not self.with_items:
self.items = []
else:
self.calculate_totals()
def calculate_totals(self):
total = base_total = 0
for item in self.get('items'):
item.amount = flt(item.rate) * flt(item.qty)
item.base_rate = flt(self.conversion_rate * item.rate)
item.base_amount = flt(self.conversion_rate * item.amount)
total += item.amount
base_total += item.base_amount
self.total = flt(total)
self.base_total = flt(base_total)
self.grand_total = flt(self.total) + flt(self.opportunity_amount)
self.base_grand_total = flt(self.base_total) + flt(self.base_opportunity_amount)
def make_new_lead_if_required(self):
"""Set lead against new opportunity"""
if (not self.get("party_name")) and self.contact_email:
@ -224,13 +240,6 @@ def make_quotation(source_name, target_doc=None):
company_currency = frappe.get_cached_value('Company', quotation.company, "default_currency")
if quotation.quotation_to == 'Customer' and quotation.party_name:
party_account_currency = get_party_account_currency("Customer", quotation.party_name, quotation.company)
else:
party_account_currency = company_currency
quotation.currency = party_account_currency or company_currency
if company_currency == quotation.currency:
exchange_rate = 1
else:
@ -254,7 +263,7 @@ def make_quotation(source_name, target_doc=None):
"doctype": "Quotation",
"field_map": {
"opportunity_from": "quotation_to",
"name": "enq_no",
"name": "enq_no"
}
},
"Opportunity Item": {

View File

@ -63,6 +63,10 @@ class TestOpportunity(unittest.TestCase):
self.assertEqual(opp_doc.opportunity_from, "Customer")
self.assertEqual(opp_doc.party_name, customer.name)
def test_opportunity_item(self):
opportunity_doc = make_opportunity(with_items=1, rate=1100, qty=2)
self.assertEqual(opportunity_doc.total, 2200)
def make_opportunity(**args):
args = frappe._dict(args)
@ -71,6 +75,7 @@ def make_opportunity(**args):
"company": args.company or "_Test Company",
"opportunity_from": args.opportunity_from or "Customer",
"opportunity_type": "Sales",
"conversion_rate": 1.0,
"with_items": args.with_items or 0,
"transaction_date": today()
})
@ -85,6 +90,7 @@ def make_opportunity(**args):
opp_doc.append('items', {
"item_code": args.item_code or "_Test Item",
"qty": args.qty or 1,
"rate": args.rate or 1000,
"uom": "_Test UOM"
})

View File

@ -1,469 +1,177 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-02-22 01:27:51",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"creation": "2013-02-22 01:27:51",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item_code",
"item_name",
"col_break1",
"uom",
"qty",
"section_break_6",
"brand",
"item_group",
"description",
"column_break_8",
"image",
"image_view",
"quantity_and_rate_section",
"base_rate",
"base_amount",
"column_break_16",
"rate",
"amount"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_code",
"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": "Item Code",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item Code",
"oldfieldname": "item_code",
"oldfieldtype": "Link",
"options": "Item"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "col_break1",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "col_break1",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "qty",
"fieldtype": "Float",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Qty",
"length": 0,
"no_copy": 0,
"oldfieldname": "qty",
"oldfieldtype": "Currency",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"default": "1",
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
"oldfieldname": "qty",
"oldfieldtype": "Currency"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "",
"fieldname": "item_group",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Item Group",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_group",
"oldfieldtype": "Link",
"options": "Item Group",
"permlevel": 0,
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "item_group",
"fieldtype": "Link",
"hidden": 1,
"label": "Item Group",
"oldfieldname": "item_group",
"oldfieldtype": "Link",
"options": "Item Group",
"print_hide": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "brand",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Brand",
"length": 0,
"no_copy": 0,
"oldfieldname": "brand",
"oldfieldtype": "Link",
"options": "Brand",
"permlevel": 0,
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "brand",
"fieldtype": "Link",
"hidden": 1,
"label": "Brand",
"oldfieldname": "brand",
"oldfieldtype": "Link",
"options": "Brand",
"print_hide": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"collapsible": 1,
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Description"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "uom",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "UOM",
"length": 0,
"no_copy": 0,
"oldfieldname": "uom",
"oldfieldtype": "Link",
"options": "UOM",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"oldfieldname": "uom",
"oldfieldtype": "Link",
"options": "UOM"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "item_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Item Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "item_name",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
"label": "Item Name",
"oldfieldname": "item_name",
"oldfieldtype": "Data"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Description",
"length": 0,
"no_copy": 0,
"oldfieldname": "description",
"oldfieldtype": "Text",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"print_width": "300px",
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0,
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Text",
"print_width": "300px",
"width": "300px"
},
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8",
"fieldtype": "Column Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_8",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Image",
"length": 0,
"no_copy": 0,
"options": "",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "image",
"fieldtype": "Attach",
"hidden": 1,
"label": "Image"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "image_view",
"fieldtype": "Image",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Image View",
"length": 0,
"no_copy": 0,
"options": "image",
"permlevel": 0,
"precision": "",
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "image_view",
"fieldtype": "Image",
"label": "Image View",
"options": "image",
"print_hide": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "basic_rate",
"fieldtype": "Currency",
"hidden": 1,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Basic Rate",
"length": 0,
"no_copy": 0,
"oldfieldname": "basic_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"permlevel": 0,
"print_hide": 1,
"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,
"translatable": 0,
"unique": 0
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Rate",
"options": "currency",
"reqd": 1
},
{
"fieldname": "quantity_and_rate_section",
"fieldtype": "Section Break",
"label": "Quantity and Rate"
},
{
"fieldname": "base_amount",
"fieldtype": "Currency",
"label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1,
"reqd": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "amount",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
"options": "currency",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "base_rate",
"fieldtype": "Currency",
"label": "Rate (Company Currency)",
"oldfieldname": "basic_rate",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
"read_only": 1,
"reqd": 1
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 1,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-12-28 15:43:09.382012",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity Item",
"owner": "Administrator",
"permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"track_changes": 1,
"track_seen": 0,
"track_views": 0
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-07-30 16:39:09.775720",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity Item",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,65 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Opportunity Summary by Sales Stage"] = {
"filters": [
{
fieldname: "based_on",
label: __("Based On"),
fieldtype: "Select",
options: "Opportunity Owner\nSource\nOpportunity Type",
default: "Opportunity Owner"
},
{
fieldname: "data_based_on",
label: __("Data Based On"),
fieldtype: "Select",
options: "Number\nAmount",
default: "Number"
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date",
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date",
},
{
fieldname: "status",
label: __("Status"),
fieldtype: "MultiSelectList",
get_data: function() {
return [
{value: "Open", description: "Status"},
{value: "Converted", description: "Status"},
{value: "Quotation", description: "Status"},
{value: "Replied", description: "Status"}
]
}
},
{
fieldname: "opportunity_source",
label: __("Oppoturnity Source"),
fieldtype: "Link",
options: "Lead Source",
},
{
fieldname: "opportunity_type",
label: __("Opportunity Type"),
fieldtype: "Link",
options: "Opportunity Type",
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company")
}
]
};

View File

@ -0,0 +1,29 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-28 12:18:24.028737",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-07-28 12:18:24.028737",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity Summary by Sales Stage",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Opportunity",
"report_name": "Opportunity Summary by Sales Stage ",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Sales Manager"
}
]
}

View File

@ -0,0 +1,254 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
import frappe
import pandas
from frappe import _
from frappe.utils import flt
from six import iteritems
from erpnext.setup.utils import get_exchange_rate
def execute(filters=None):
return OpportunitySummaryBySalesStage(filters).run()
class OpportunitySummaryBySalesStage(object):
def __init__(self,filters=None):
self.filters = frappe._dict(filters or {})
def run(self):
self.get_columns()
self.get_data()
self.get_chart_data()
return self.columns, self.data, None, self.chart
def get_columns(self):
self.columns = []
if self.filters.get('based_on') == 'Opportunity Owner':
self.columns.append({
'label': _('Opportunity Owner'),
'fieldname': 'opportunity_owner',
'width': 200
})
if self.filters.get('based_on') == 'Source':
self.columns.append({
'label': _('Source'),
'fieldname': 'source',
'fieldtype': 'Link',
'options': 'Lead Source',
'width': 200
})
if self.filters.get('based_on') == 'Opportunity Type':
self.columns.append({
'label': _('Opportunity Type'),
'fieldname': 'opportunity_type',
'width': 200
})
self.set_sales_stage_columns()
def set_sales_stage_columns(self):
self.sales_stage_list = frappe.db.get_list('Sales Stage', pluck='name')
for sales_stage in self.sales_stage_list:
if self.filters.get('data_based_on') == 'Number':
self.columns.append({
'label': _(sales_stage),
'fieldname': sales_stage,
'fieldtype': 'Int',
'width': 150
})
elif self.filters.get('data_based_on') == 'Amount':
self.columns.append({
'label': _(sales_stage),
'fieldname': sales_stage,
'fieldtype': 'Currency',
'width': 150
})
def get_data(self):
self.data = []
based_on = {
'Opportunity Owner': '_assign',
'Source': 'source',
'Opportunity Type': 'opportunity_type'
}[self.filters.get('based_on')]
data_based_on = {
'Number': 'count(name) as count',
'Amount': 'opportunity_amount as amount',
}[self.filters.get('data_based_on')]
self.get_data_query(based_on, data_based_on)
self.get_rows()
def get_data_query(self, based_on, data_based_on):
if self.filters.get('data_based_on') == 'Number':
group_by = '{},{}'.format('sales_stage', based_on)
self.query_result = frappe.db.get_list('Opportunity',
filters=self.get_conditions(),
fields=['sales_stage', data_based_on, based_on],
group_by=group_by
)
elif self.filters.get('data_based_on') == 'Amount':
self.query_result = frappe.db.get_list('Opportunity',
filters=self.get_conditions(),
fields=['sales_stage', based_on, data_based_on, 'currency']
)
self.convert_to_base_currency()
dataframe = pandas.DataFrame.from_records(self.query_result)
dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True)
result = dataframe.groupby(['sales_stage', based_on], as_index=False)['amount'].sum()
self.grouped_data = []
for i in range(len(result['amount'])):
self.grouped_data.append({
'sales_stage': result['sales_stage'][i],
based_on : result[based_on][i],
'amount': result['amount'][i]
})
self.query_result = self.grouped_data
def get_rows(self):
self.data = []
self.get_formatted_data()
for based_on,data in iteritems(self.formatted_data):
row_based_on={
'Opportunity Owner': 'opportunity_owner',
'Source': 'source',
'Opportunity Type': 'opportunity_type'
}[self.filters.get('based_on')]
row = {row_based_on: based_on}
for d in self.query_result:
sales_stage = d.get('sales_stage')
row[sales_stage] = data.get(sales_stage)
self.data.append(row)
def get_formatted_data(self):
self.formatted_data = frappe._dict()
for d in self.query_result:
data_based_on ={
'Number': 'count',
'Amount': 'amount'
}[self.filters.get('data_based_on')]
based_on ={
'Opportunity Owner': '_assign',
'Source': 'source',
'Opportunity Type': 'opportunity_type'
}[self.filters.get('based_on')]
if self.filters.get('based_on') == 'Opportunity Owner':
if d.get(based_on) == '[]' or d.get(based_on) is None or d.get(based_on) == 'Not Assigned':
assignments = ['Not Assigned']
else:
assignments = json.loads(d.get(based_on))
sales_stage = d.get('sales_stage')
count = d.get(data_based_on)
if assignments:
if len(assignments) > 1:
for assigned_to in assignments:
self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count)
else:
assigned_to = assignments[0]
self.set_formatted_data_based_on_sales_stage(assigned_to, sales_stage, count)
else:
value = d.get(based_on)
sales_stage = d.get('sales_stage')
count = d.get(data_based_on)
self.set_formatted_data_based_on_sales_stage(value, sales_stage, count)
def set_formatted_data_based_on_sales_stage(self, based_on, sales_stage, count):
self.formatted_data.setdefault(based_on, frappe._dict()).setdefault(sales_stage, 0)
self.formatted_data[based_on][sales_stage] += count
def get_conditions(self):
filters = []
if self.filters.get('company'):
filters.append({'company': self.filters.get('company')})
if self.filters.get('opportunity_type'):
filters.append({'opportunity_type': self.filters.get('opportunity_type')})
if self.filters.get('opportunity_source'):
filters.append({'source': self.filters.get('opportunity_source')})
if self.filters.get('status'):
filters.append({'status': ('in',self.filters.get('status'))})
if self.filters.get('from_date') and self.filters.get('to_date'):
filters.append(['transaction_date', 'between', [self.filters.get('from_date'), self.filters.get('to_date')]])
return filters
def get_chart_data(self):
labels = []
datasets = []
values = [0] * 8
for sales_stage in self.sales_stage_list:
labels.append(sales_stage)
options = {
'Number': 'count',
'Amount': 'amount'
}[self.filters.get('data_based_on')]
for data in self.query_result:
for count in range(len(values)):
if data['sales_stage'] == labels[count]:
values[count] = values[count] + data[options]
datasets.append({'name':options, 'values':values})
self.chart = {
'data':{
'labels': labels,
'datasets': datasets
},
'type':'line'
}
def currency_conversion(self,from_currency,to_currency):
cacheobj = frappe.cache()
if cacheobj.get(from_currency):
return flt(str(cacheobj.get(from_currency),'UTF-8'))
else:
value = get_exchange_rate(from_currency,to_currency)
cacheobj.set(from_currency,value)
return flt(str(cacheobj.get(from_currency),'UTF-8'))
def get_default_currency(self):
company = self.filters.get('company')
return frappe.db.get_value('Company', company, 'default_currency')
def convert_to_base_currency(self):
default_currency = self.get_default_currency()
for data in self.query_result:
if data.get('currency') != default_currency:
opportunity_currency = data.get('currency')
value = self.currency_conversion(opportunity_currency,default_currency)
data['amount'] = data['amount'] * value

View File

@ -0,0 +1,94 @@
import unittest
import frappe
from erpnext.crm.report.opportunity_summary_by_sales_stage.opportunity_summary_by_sales_stage import (
execute,
)
from erpnext.crm.report.sales_pipeline_analytics.test_sales_pipeline_analytics import (
create_company,
create_customer,
create_opportunity,
)
class TestOpportunitySummaryBySalesStage(unittest.TestCase):
@classmethod
def setUpClass(self):
frappe.db.delete("Opportunity")
create_company()
create_customer()
create_opportunity()
def test_opportunity_summary_by_sales_stage(self):
self.check_for_opportunity_owner()
self.check_for_source()
self.check_for_opportunity_type()
self.check_all_filters()
def check_for_opportunity_owner(self):
filters = {
'based_on': "Opportunity Owner",
'data_based_on': "Number",
'company': "Best Test"
}
report = execute(filters)
expected_data = [{
'opportunity_owner': "Not Assigned",
'Prospecting': 1
}]
self.assertEqual(expected_data, report[1])
def check_for_source(self):
filters = {
'based_on': "Source",
'data_based_on': "Number",
'company': "Best Test"
}
report = execute(filters)
expected_data = [{
'source': 'Cold Calling',
'Prospecting': 1
}]
self.assertEqual(expected_data, report[1])
def check_for_opportunity_type(self):
filters = {
'based_on': "Opportunity Type",
'data_based_on': "Number",
'company': "Best Test"
}
report = execute(filters)
expected_data = [{
'opportunity_type': 'Sales',
'Prospecting': 1
}]
self.assertEqual(expected_data, report[1])
def check_all_filters(self):
filters = {
'based_on': "Opportunity Type",
'data_based_on': "Number",
'company': "Best Test",
'opportunity_source': "Cold Calling",
'opportunity_type': "Sales",
'status': ["Open"]
}
report = execute(filters)
expected_data = [{
'opportunity_type': 'Sales',
'Prospecting': 1
}]
self.assertEqual(expected_data, report[1])

View File

@ -0,0 +1,70 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
/* eslint-disable */
frappe.query_reports["Sales Pipeline Analytics"] = {
"filters": [
{
fieldname: "pipeline_by",
label: __("Pipeline By"),
fieldtype: "Select",
options: "Owner\nSales Stage",
default: "Owner"
},
{
fieldname: "from_date",
label: __("From Date"),
fieldtype: "Date"
},
{
fieldname: "to_date",
label: __("To Date"),
fieldtype: "Date"
},
{
fieldname: "range",
label: __("Range"),
fieldtype: "Select",
options: "Monthly\nQuarterly",
default: "Monthly"
},
{
fieldname: "assigned_to",
label: __("Assigned To"),
fieldtype: "Link",
options: "User"
},
{
fieldname: "status",
label: __("Status"),
fieldtype: "Select",
options: "Open\nQuotation\nConverted\nReplied"
},
{
fieldname: "based_on",
label: __("Based On"),
fieldtype: "Select",
options: "Number\nAmount",
default: "Number"
},
{
fieldname: "company",
label: __("Company"),
fieldtype: "Link",
options: "Company",
default: frappe.defaults.get_user_default("Company")
},
{
fieldname: "opportunity_source",
label: __("Opportunity Source"),
fieldtype: "Link",
options: "Lead Source"
},
{
fieldname: "opportunity_type",
label: __("Opportunity Type"),
fieldtype: "Link",
options: "Opportunity Type"
},
]
};

View File

@ -0,0 +1,29 @@
{
"add_total_row": 0,
"columns": [],
"creation": "2021-07-01 17:29:09.530787",
"disable_prepared_report": 0,
"disabled": 0,
"docstatus": 0,
"doctype": "Report",
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-07-01 17:45:17.612861",
"modified_by": "Administrator",
"module": "CRM",
"name": "Sales Pipeline Analytics",
"owner": "Administrator",
"prepared_report": 0,
"ref_doctype": "Opportunity",
"report_name": "Sales Pipeline Analytics",
"report_type": "Script Report",
"roles": [
{
"role": "Sales User"
},
{
"role": "Sales Manager"
}
]
}

View File

@ -0,0 +1,333 @@
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import json
from datetime import date
import frappe
import pandas
from dateutil.relativedelta import relativedelta
from frappe import _
from frappe.utils import cint, flt
from six import iteritems
from erpnext.setup.utils import get_exchange_rate
def execute(filters=None):
return SalesPipelineAnalytics(filters).run()
class SalesPipelineAnalytics(object):
def __init__(self, filters=None):
self.filters = frappe._dict(filters or {})
def run(self):
self.get_columns()
self.get_data()
self.get_chart_data()
return self.columns, self.data, None, self.chart
def get_columns(self):
self.columns = []
self.set_range_columns()
self.set_pipeline_based_on_column()
def set_range_columns(self):
based_on = {
'Number': 'Int',
'Amount': 'Currency'
}[self.filters.get('based_on')]
if self.filters.get('range') == 'Monthly':
month_list = self.get_month_list()
for month in month_list:
self.columns.append({
'fieldname': month,
'fieldtype': based_on,
'label': month,
'width': 200
})
elif self.filters.get('range') == 'Quarterly':
for quarter in range(1, 5):
self.columns.append({
'fieldname': f'Q{quarter}',
'fieldtype': based_on,
'label': f'Q{quarter}',
'width': 200
})
def set_pipeline_based_on_column(self):
if self.filters.get('pipeline_by') == 'Owner':
self.columns.insert(0, {
'fieldname': 'opportunity_owner',
'label': _('Opportunity Owner'),
'width': 200
})
elif self.filters.get('pipeline_by') == 'Sales Stage':
self.columns.insert(0, {
'fieldname': 'sales_stage',
'label': _('Sales Stage'),
'width': 200
})
def get_fields(self):
self.based_on ={
'Owner': '_assign as opportunity_owner',
'Sales Stage': 'sales_stage'
}[self.filters.get('pipeline_by')]
self.data_based_on ={
'Number': 'count(name) as count',
'Amount': 'opportunity_amount as amount'
}[self.filters.get('based_on')]
self.group_by_based_on = {
'Owner': '_assign',
'Sales Stage': 'sales_stage'
}[self.filters.get('pipeline_by')]
self.group_by_period = {
'Monthly': 'month(expected_closing)',
'Quarterly': 'QUARTER(expected_closing)'
}[self.filters.get('range')]
self.pipeline_by = {
'Owner': 'opportunity_owner',
'Sales Stage': 'sales_stage'
}[self.filters.get('pipeline_by')]
self.duration = {
'Monthly': 'monthname(expected_closing) as month',
'Quarterly': 'QUARTER(expected_closing) as quarter'
}[self.filters.get('range')]
self.period_by = {
'Monthly': 'month',
'Quarterly': 'quarter'
}[self.filters.get('range')]
def get_data(self):
self.get_fields()
if self.filters.get('based_on') == 'Number':
self.query_result = frappe.db.get_list('Opportunity',
filters=self.get_conditions(),
fields=[self.based_on, self.data_based_on, self.duration],
group_by='{},{}'.format(self.group_by_based_on, self.group_by_period),
order_by=self.group_by_period
)
if self.filters.get('based_on') == 'Amount':
self.query_result = frappe.db.get_list('Opportunity',
filters=self.get_conditions(),
fields=[self.based_on, self.data_based_on, self.duration, 'currency']
)
self.convert_to_base_currency()
dataframe = pandas.DataFrame.from_records(self.query_result)
dataframe.replace(to_replace=[None], value='Not Assigned', inplace=True)
result = dataframe.groupby([self.pipeline_by, self.period_by], as_index=False)['amount'].sum()
self.grouped_data = []
for i in range(len(result['amount'])):
self.grouped_data.append({
self.pipeline_by : result[self.pipeline_by][i],
self.period_by : result[self.period_by][i],
'amount': result['amount'][i]
})
self.query_result = self.grouped_data
self.get_periodic_data()
self.append_data(self.pipeline_by, self.period_by)
def get_conditions(self):
conditions = []
if self.filters.get('opportunity_source'):
conditions.append({'source': self.filters.get('opportunity_source')})
if self.filters.get('opportunity_type'):
conditions.append({'opportunity_type': self.filters.get('opportunity_type')})
if self.filters.get('status'):
conditions.append({'status': self.filters.get('status')})
if self.filters.get('company'):
conditions.append({'company': self.filters.get('company')})
if self.filters.get('from_date') and self.filters.get('to_date'):
conditions.append(['expected_closing', 'between',
[self.filters.get('from_date'), self.filters.get('to_date')]])
return conditions
def get_chart_data(self):
labels = []
datasets = []
self.append_to_dataset(datasets)
for column in self.columns:
if column['fieldname'] != 'opportunity_owner' and column['fieldname'] != 'sales_stage':
labels.append(column['fieldname'])
self.chart = {
'data':{
'labels': labels,
'datasets': datasets
},
'type':'line'
}
return self.chart
def get_periodic_data(self):
self.periodic_data = frappe._dict()
based_on = {
'Number': 'count',
'Amount': 'amount'
}[self.filters.get('based_on')]
pipeline_by = {
'Owner': 'opportunity_owner',
'Sales Stage': 'sales_stage'
}[self.filters.get('pipeline_by')]
frequency = {
'Monthly': 'month',
'Quarterly': 'quarter'
}[self.filters.get('range')]
for info in self.query_result:
if self.filters.get('range') == 'Monthly':
period = info.get(frequency)
if self.filters.get('range') == 'Quarterly':
period = f'Q{cint(info.get("quarter"))}'
value = info.get(pipeline_by)
count_or_amount = info.get(based_on)
if self.filters.get('pipeline_by') == 'Owner':
if value == 'Not Assigned' or value == '[]' or value is None:
assigned_to = ['Not Assigned']
else:
assigned_to = json.loads(value)
self.check_for_assigned_to(period, value, count_or_amount, assigned_to, info)
else:
self.set_formatted_data(period, value, count_or_amount, None)
def set_formatted_data(self, period, value, count_or_amount, assigned_to):
if assigned_to:
if len(assigned_to) > 1:
if self.filters.get('assigned_to'):
for user in assigned_to:
if self.filters.get('assigned_to') == user:
value = user
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
self.periodic_data[value][period] += count_or_amount
else:
for user in assigned_to:
value = user
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
self.periodic_data[value][period] += count_or_amount
else:
value = assigned_to[0]
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
self.periodic_data[value][period] += count_or_amount
else:
self.periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0)
self.periodic_data[value][period] += count_or_amount
def check_for_assigned_to(self, period, value, count_or_amount, assigned_to, info):
if self.filters.get('assigned_to'):
for data in json.loads(info.get('opportunity_owner')):
if data == self.filters.get('assigned_to'):
self.set_formatted_data(period, data, count_or_amount, assigned_to)
else:
self.set_formatted_data(period, value, count_or_amount, assigned_to)
def get_month_list(self):
month_list= []
current_date = date.today()
month_number = date.today().month
for month in range(month_number,13):
month_list.append(current_date.strftime('%B'))
current_date = current_date + relativedelta(months=1)
return month_list
def append_to_dataset(self, datasets):
range_by = {
'Monthly': 'month',
'Quarterly': 'quarter'
}[self.filters.get('range')]
based_on = {
'Amount': 'amount',
'Number': 'count'
}[self.filters.get('based_on')]
if self.filters.get('range') == 'Quarterly':
frequency_list = [1,2,3,4]
count = [0] * 4
if self.filters.get('range') == 'Monthly':
frequency_list = self.get_month_list()
count = [0] * 12
for info in self.query_result:
for i in range(len(frequency_list)):
if info[range_by] == frequency_list[i]:
count[i] = count[i] + info[based_on]
datasets.append({'name': based_on, 'values': count})
def append_data(self, pipeline_by, period_by):
self.data = []
for pipeline,period_data in iteritems(self.periodic_data):
row = {pipeline_by : pipeline}
for info in self.query_result:
if self.filters.get('range') == 'Monthly':
period = info.get(period_by)
if self.filters.get('range') == 'Quarterly':
period = f'Q{cint(info.get(period_by))}'
count = period_data.get(period,0.0)
row[period] = count
self.data.append(row)
def get_default_currency(self):
company = self.filters.get('company')
return frappe.db.get_value('Company',company,['default_currency'])
def get_currency_rate(self, from_currency, to_currency):
cacheobj = frappe.cache()
if cacheobj.get(from_currency):
return flt(str(cacheobj.get(from_currency),'UTF-8'))
else:
value = get_exchange_rate(from_currency, to_currency)
cacheobj.set(from_currency, value)
return flt(str(cacheobj.get(from_currency),'UTF-8'))
def convert_to_base_currency(self):
default_currency = self.get_default_currency()
for data in self.query_result:
if data.get('currency') != default_currency:
opportunity_currency = data.get('currency')
value = self.get_currency_rate(opportunity_currency,default_currency)
data['amount'] = data['amount'] * value

View File

@ -0,0 +1,238 @@
import unittest
import frappe
from erpnext.crm.report.sales_pipeline_analytics.sales_pipeline_analytics import execute
class TestSalesPipelineAnalytics(unittest.TestCase):
@classmethod
def setUpClass(self):
frappe.db.delete("Opportunity")
create_company()
create_customer()
create_opportunity()
def test_sales_pipeline_analytics(self):
self.check_for_monthly_and_number()
self.check_for_monthly_and_amount()
self.check_for_quarterly_and_number()
self.check_for_quarterly_and_amount()
self.check_for_all_filters()
def check_for_monthly_and_number(self):
filters = {
'pipeline_by':"Owner",
'range':"Monthly",
'based_on':"Number",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'opportunity_owner':'Not Assigned',
'August':1
}
]
self.assertEqual(expected_data,report[1])
filters = {
'pipeline_by':"Sales Stage",
'range':"Monthly",
'based_on':"Number",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'sales_stage':'Prospecting',
'August':1
}
]
self.assertEqual(expected_data,report[1])
def check_for_monthly_and_amount(self):
filters = {
'pipeline_by':"Owner",
'range':"Monthly",
'based_on':"Amount",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'opportunity_owner':'Not Assigned',
'August':150000
}
]
self.assertEqual(expected_data,report[1])
filters = {
'pipeline_by':"Sales Stage",
'range':"Monthly",
'based_on':"Amount",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'sales_stage':'Prospecting',
'August':150000
}
]
self.assertEqual(expected_data,report[1])
def check_for_quarterly_and_number(self):
filters = {
'pipeline_by':"Owner",
'range':"Quarterly",
'based_on':"Number",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'opportunity_owner':'Not Assigned',
'Q3':1
}
]
self.assertEqual(expected_data,report[1])
filters = {
'pipeline_by':"Sales Stage",
'range':"Quarterly",
'based_on':"Number",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'sales_stage':'Prospecting',
'Q3':1
}
]
self.assertEqual(expected_data,report[1])
def check_for_quarterly_and_amount(self):
filters = {
'pipeline_by':"Owner",
'range':"Quarterly",
'based_on':"Amount",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'opportunity_owner':'Not Assigned',
'Q3':150000
}
]
self.assertEqual(expected_data,report[1])
filters = {
'pipeline_by':"Sales Stage",
'range':"Quarterly",
'based_on':"Amount",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test"
}
report = execute(filters)
expected_data = [
{
'sales_stage':'Prospecting',
'Q3':150000
}
]
self.assertEqual(expected_data,report[1])
def check_for_all_filters(self):
filters = {
'pipeline_by':"Owner",
'range':"Monthly",
'based_on':"Number",
'status':"Open",
'opportunity_type':"Sales",
'company':"Best Test",
'opportunity_source':'Cold Calling',
'from_date': '2021-08-01',
'to_date':'2021-08-31'
}
report = execute(filters)
expected_data = [
{
'opportunity_owner':'Not Assigned',
'August': 1
}
]
self.assertEqual(expected_data,report[1])
def create_company():
doc = frappe.db.exists('Company','Best Test')
if not doc:
doc = frappe.new_doc('Company')
doc.company_name = 'Best Test'
doc.default_currency = "INR"
doc.insert()
def create_customer():
doc = frappe.db.exists("Customer","_Test NC")
if not doc:
doc = frappe.new_doc("Customer")
doc.customer_name = '_Test NC'
doc.insert()
def create_opportunity():
doc = frappe.db.exists({"doctype":"Opportunity","party_name":"_Test NC"})
if not doc:
doc = frappe.new_doc("Opportunity")
doc.opportunity_from = "Customer"
customer_name = frappe.db.get_value("Customer",{"customer_name":'_Test NC'},['customer_name'])
doc.party_name = customer_name
doc.opportunity_amount = 150000
doc.source = "Cold Calling"
doc.currency = "INR"
doc.expected_closing = "2021-08-31"
doc.company = 'Best Test'
doc.insert()

View File

@ -147,6 +147,24 @@
"onboard": 1,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Sales Pipeline Analytics",
"link_to": "Sales Pipeline Analytics",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Opportunity Summary by Sales Stage",
"link_to": "Opportunity Summary by Sales Stage",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
@ -403,7 +421,7 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:56.913091",
"modified": "2021-08-19 19:08:08.728876",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",

View File

@ -85,10 +85,8 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype:
add_account_subtype(account["subtype"])
existing_bank_account = frappe.db.exists("Bank Account", {
'account_name': account["name"],
'bank': bank["bank_name"]
})
bank_account_name = "{} - {}".format(account["name"], bank["bank_name"])
existing_bank_account = frappe.db.exists("Bank Account", bank_account_name)
if not existing_bank_account:
try:
@ -197,6 +195,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
plaid = PlaidConnector(access_token)
transactions = []
try:
transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
except ItemError as e:
@ -205,7 +204,7 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
return transactions or []
return transactions
def new_bank_transaction(transaction):

View File

@ -62,6 +62,7 @@ treeviews = ['Account', 'Cost Center', 'Warehouse', 'Item Group', 'Customer Grou
# website
update_website_context = ["erpnext.shopping_cart.utils.update_website_context", "erpnext.education.doctype.education_settings.education_settings.update_website_context"]
my_account_context = "erpnext.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "Course Schedule"]
@ -441,7 +442,7 @@ accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice"
"Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
"Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
"Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
"Subscription Plan"
"Subscription Plan", "POS Invoice", "POS Invoice Item"
]
regional_overrides = {

View File

@ -13,7 +13,7 @@ from erpnext.hr.utils import get_holidays_for_employee
# HOLIDAY REMINDERS
# -----------------
def send_reminders_in_advance_weekly():
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
frequency = frappe.db.get_single_value("HR Settings", "frequency")
if not (to_send_in_advance and frequency == "Weekly"):
return
@ -21,7 +21,7 @@ def send_reminders_in_advance_weekly():
send_advance_holiday_reminders("Weekly")
def send_reminders_in_advance_monthly():
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders") or 1)
to_send_in_advance = int(frappe.db.get_single_value("HR Settings", "send_holiday_reminders"))
frequency = frappe.db.get_single_value("HR Settings", "frequency")
if not (to_send_in_advance and frequency == "Monthly"):
return
@ -79,7 +79,7 @@ def send_holidays_reminder_in_advance(employee, holidays):
# ------------------
def send_birthday_reminders():
"""Send Employee birthday reminders if no 'Stop Birthday Reminders' is not set."""
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders") or 1)
to_send = int(frappe.db.get_single_value("HR Settings", "send_birthday_reminders"))
if not to_send:
return
@ -184,7 +184,7 @@ def get_employees_having_an_event_today(event_type):
# --------------------------
def send_work_anniversary_reminders():
"""Send Employee Work Anniversary Reminders if 'Send Work Anniversary Reminders' is checked"""
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders") or 1)
to_send = int(frappe.db.get_single_value("HR Settings", "send_work_anniversary_reminders"))
if not to_send:
return

View File

@ -73,7 +73,7 @@ frappe.ui.form.on('Employee Advance', {
frm.trigger('make_return_entry');
}, __('Create'));
} else if (frm.doc.repay_unclaimed_amount_from_salary == 1 && frappe.model.can_create("Additional Salary")) {
frm.add_custom_button(__("Deduction from salary"), function() {
frm.add_custom_button(__("Deduction from Salary"), function() {
frm.events.make_deduction_via_additional_salary(frm);
}, __('Create'));
}

View File

@ -170,7 +170,7 @@
"default": "0",
"fieldname": "repay_unclaimed_amount_from_salary",
"fieldtype": "Check",
"label": "Repay unclaimed amount from salary"
"label": "Repay Unclaimed Amount from Salary"
},
{
"depends_on": "eval:cur_frm.doc.employee",
@ -200,10 +200,11 @@
],
"is_submittable": 1,
"links": [],
"modified": "2021-03-31 22:31:53.746659",
"modified": "2021-09-11 18:38:38.617478",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{

View File

@ -172,7 +172,10 @@ def get_paying_amount_paying_exchange_rate(payment_account, doc):
@frappe.whitelist()
def create_return_through_additional_salary(doc):
import json
doc = frappe._dict(json.loads(doc))
if isinstance(doc, str):
doc = frappe._dict(json.loads(doc))
additional_salary = frappe.new_doc('Additional Salary')
additional_salary.employee = doc.employee
additional_salary.currency = doc.currency

View File

@ -12,8 +12,11 @@ import erpnext
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.employee_advance.employee_advance import (
EmployeeAdvanceOverPayment,
create_return_through_additional_salary,
make_bank_entry,
)
from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
class TestEmployeeAdvance(unittest.TestCase):
@ -33,6 +36,46 @@ class TestEmployeeAdvance(unittest.TestCase):
journal_entry1 = make_payment_entry(advance)
self.assertRaises(EmployeeAdvanceOverPayment, journal_entry1.submit)
def test_repay_unclaimed_amount_from_salary(self):
employee_name = make_employee("_T@employe.advance")
advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1})
args = {"type": "Deduction"}
create_salary_component("Advance Salary - Deduction", **args)
make_salary_structure("Test Additional Salary for Advance Return", "Monthly", employee=employee_name)
# additional salary for 700 first
advance.reload()
additional_salary = create_return_through_additional_salary(advance)
additional_salary.salary_component = "Advance Salary - Deduction"
additional_salary.payroll_date = nowdate()
additional_salary.amount = 700
additional_salary.insert()
additional_salary.submit()
advance.reload()
self.assertEqual(advance.return_amount, 700)
# additional salary for remaining 300
additional_salary = create_return_through_additional_salary(advance)
additional_salary.salary_component = "Advance Salary - Deduction"
additional_salary.payroll_date = nowdate()
additional_salary.amount = 300
additional_salary.insert()
additional_salary.submit()
advance.reload()
self.assertEqual(advance.return_amount, 1000)
# update advance return amount on additional salary cancellation
additional_salary.cancel()
advance.reload()
self.assertEqual(advance.return_amount, 700)
def tearDown(self):
frappe.db.rollback()
def make_payment_entry(advance):
journal_entry = frappe.get_doc(make_bank_entry("Employee Advance", advance.name))
journal_entry.cheque_no = "123123"
@ -41,7 +84,7 @@ def make_payment_entry(advance):
return journal_entry
def make_employee_advance(employee_name):
def make_employee_advance(employee_name, args=None):
doc = frappe.new_doc("Employee Advance")
doc.employee = employee_name
doc.company = "_Test company"
@ -51,6 +94,10 @@ def make_employee_advance(employee_name):
doc.advance_amount = 1000
doc.posting_date = nowdate()
doc.advance_account = "_Test Employee Advance - _TC"
if args:
doc.update(args)
doc.insert()
doc.submit()

View File

@ -55,8 +55,7 @@ def mark_employee_attendance(employee_list, status, date, leave_type=None, compa
else:
leave_type = None
if not company:
company = frappe.db.get_value("Employee", employee['employee'], "Company")
company = frappe.db.get_value("Employee", employee['employee'], "Company", cache=True)
attendance=frappe.get_doc(dict(
doctype='Attendance',
@ -68,4 +67,4 @@ def mark_employee_attendance(employee_list, status, date, leave_type=None, compa
company=company
))
attendance.insert()
attendance.submit()
attendance.submit()

View File

@ -21,7 +21,7 @@ frappe.ui.form.on('Training Result', {
frm.set_value("employees" ,"");
if (r.message) {
$.each(r.message, function(i, d) {
var row = frappe.model.add_child(cur_frm.doc, "Training Result Employee", "employees");
var row = frappe.model.add_child(frm.doc, "Training Result Employee", "employees");
row.employee = d.employee;
row.employee_name = d.employee_name;
});

View File

@ -32,7 +32,10 @@ def set_employee_name(doc):
def update_employee(employee, details, date=None, cancel=False):
internal_work_history = {}
for item in details:
fieldtype = frappe.get_meta("Employee").get_field(item.fieldname).fieldtype
field = frappe.get_meta("Employee").get_field(item.fieldname)
if not field:
continue
fieldtype = field.fieldtype
new_data = item.new if not cancel else item.current
if fieldtype == "Date" and new_data:
new_data = getdate(new_data)

View File

@ -18,7 +18,7 @@ frappe.ui.form.on('Maintenance Schedule', {
},
refresh: function (frm) {
setTimeout(() => {
frm.toggle_display('generate_schedule', !(frm.is_new()));
frm.toggle_display('generate_schedule', !(frm.is_new() || frm.doc.docstatus));
frm.toggle_display('schedule', !(frm.is_new()));
}, 10);
},

View File

@ -16,9 +16,9 @@ from erpnext.utilities.transaction_base import TransactionBase, delete_events
class MaintenanceSchedule(TransactionBase):
@frappe.whitelist()
def generate_schedule(self):
if self.docstatus != 0:
return
self.set('schedules', [])
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
where parent=%s""", (self.name))
count = 1
for d in self.get('items'):
self.validate_maintenance_detail()

View File

@ -89,13 +89,14 @@
"width": "160px"
},
{
"allow_on_submit": 1,
"columns": 2,
"default": "Pending",
"fieldname": "completion_status",
"fieldtype": "Select",
"in_list_view": 1,
"label": "Completion Status",
"options": "Pending\nPartially Completed\nFully Completed",
"read_only": 1
"options": "Pending\nPartially Completed\nFully Completed"
},
{
"fieldname": "column_break_3",
@ -125,10 +126,11 @@
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-05-27 16:07:25.905015",
"modified": "2021-09-16 21:25:22.506485",
"modified_by": "Administrator",
"module": "Maintenance",
"name": "Maintenance Schedule Detail",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",

View File

@ -31,8 +31,8 @@ frappe.ui.form.on('Maintenance Visit', {
},
onload: function (frm, cdt, cdn) {
let item = locals[cdt][cdn];
if (frm.maintenance_type == 'Scheduled') {
let schedule_id = item.purposes[0].prevdoc_detail_docname;
if (frm.doc.maintenance_type === "Scheduled") {
const schedule_id = item.purposes[0].prevdoc_detail_docname || frm.doc.maintenance_schedule_detail;
frappe.call({
method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.update_serial_nos",
args: {
@ -43,6 +43,9 @@ frappe.ui.form.on('Maintenance Visit', {
}
});
}
else {
frm.clear_table("purposes");
}
if (!frm.doc.status) {
frm.set_value({ status: 'Draft' });

View File

@ -510,8 +510,14 @@ class BOM(WebsiteGenerator):
if d.workstation:
self.update_rate_and_time(d, update_hour_rate)
self.operating_cost += flt(d.operating_cost)
self.base_operating_cost += flt(d.base_operating_cost)
operating_cost = d.operating_cost
base_operating_cost = d.base_operating_cost
if d.set_cost_based_on_bom_qty:
operating_cost = flt(d.cost_per_unit) * flt(self.quantity)
base_operating_cost = flt(d.base_cost_per_unit) * flt(self.quantity)
self.operating_cost += flt(operating_cost)
self.base_operating_cost += flt(base_operating_cost)
def update_rate_and_time(self, row, update_hour_rate = False):
if not row.hour_rate or update_hour_rate:
@ -535,6 +541,8 @@ class BOM(WebsiteGenerator):
row.base_hour_rate = flt(row.hour_rate) * flt(self.conversion_rate)
row.operating_cost = flt(row.hour_rate) * flt(row.time_in_mins) / 60.0
row.base_operating_cost = flt(row.operating_cost) * flt(self.conversion_rate)
row.cost_per_unit = row.operating_cost / (row.batch_size or 1.0)
row.base_cost_per_unit = row.base_operating_cost / (row.batch_size or 1.0)
if update_hour_rate:
row.db_update()
@ -1125,7 +1133,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"):
query_filters["is_stock_item"] = 1
or_cond_filters["is_stock_item"] = 1
or_cond_filters["has_variants"] = 1
return frappe.get_list("Item",
fields = fields, filters=query_filters,

View File

@ -108,6 +108,24 @@ class TestBOM(unittest.TestCase):
self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost)
self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost)
def test_bom_cost_with_batch_size(self):
bom = frappe.copy_doc(test_records[2])
bom.docstatus = 0
op_cost = 0.0
for op_row in bom.operations:
op_row.docstatus = 0
op_row.batch_size = 2
op_row.set_cost_based_on_bom_qty = 1
op_cost += op_row.operating_cost
bom.save()
for op_row in bom.operations:
self.assertAlmostEqual(op_row.cost_per_unit, op_row.operating_cost / 2)
self.assertAlmostEqual(bom.operating_cost, op_cost/2)
bom.delete()
def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self):
frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1)
for item_code, rate in (("_Test Item", 3600), ("_Test Item Home Desktop Manufactured", 3000)):

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