Merge remote-tracking branch 'upstream/develop' into payments-based-dunning

This commit is contained in:
barredterra
2021-09-30 16:04:56 +02:00
70 changed files with 1387 additions and 1197 deletions

View File

@@ -6,7 +6,7 @@ context('Organizational Chart', () => {
it('navigates to org chart', () => {
cy.visit('/app');
cy.awesomebar('Organizational Chart');
cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {

View File

@@ -7,7 +7,7 @@ context('Organizational Chart Mobile', () => {
it('navigates to org chart', () => {
cy.viewport(375, 667);
cy.visit('/app');
cy.awesomebar('Organizational Chart');
cy.visit('/app/organizational-chart');
cy.url().should('include', '/organizational-chart');
cy.window().its('frappe.csrf_token').then(csrf_token => {

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

@@ -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

@@ -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

@@ -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

@@ -1651,7 +1651,7 @@
"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
},
@@ -1953,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
@@ -2022,11 +2023,12 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-09-08 15:24:25.486499",
"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

@@ -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

@@ -100,6 +100,7 @@ def get_tax_withholding_details(tax_withholding_category, posting_date, company)
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,
@@ -206,18 +207,39 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
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],
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')
})
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

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
@@ -390,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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -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

@@ -1133,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

@@ -677,7 +677,7 @@ def get_job_details(start, end, filters=None):
conditions = get_filters_cond("Job Card", filters, [])
job_cards = frappe.db.sql(""" SELECT `tabJob Card`.name, `tabJob Card`.work_order,
`tabJob Card`.employee_name, `tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
`tabJob Card`.status, ifnull(`tabJob Card`.remarks, ''),
min(`tabJob Card Time Log`.from_time) as from_time,
max(`tabJob Card Time Log`.to_time) as to_time
FROM `tabJob Card` , `tabJob Card Time Log`
@@ -687,7 +687,7 @@ def get_job_details(start, end, filters=None):
for d in job_cards:
subject_data = []
for field in ["name", "work_order", "remarks", "employee_name"]:
for field in ["name", "work_order", "remarks"]:
if not d.get(field): continue
subject_data.append(d.get(field))

View File

@@ -457,7 +457,8 @@ class ProductionPlan(Document):
def prepare_args_for_sub_assembly_items(self, row, args):
for field in ["production_item", "item_name", "qty", "fg_warehouse",
"description", "bom_no", "stock_uom", "bom_level", "production_plan_item"]:
"description", "bom_no", "stock_uom", "bom_level",
"production_plan_item", "schedule_date"]:
args[field] = row.get(field)
args.update({

View File

@@ -307,4 +307,9 @@ erpnext.patches.v14_0.delete_shopify_doctypes
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v14_0.update_opportunity_currency_fields
erpnext.patches.v13_0.gst_fields_for_pos_invoice
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.create_custom_field_for_finance_book
erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table

View File

@@ -0,0 +1,21 @@
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
custom_field = {
'Finance Book': [
{
'fieldname': 'for_income_tax',
'label': 'For Income Tax',
'fieldtype': 'Check',
'insert_after': 'finance_book_name',
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
}
]
}
create_custom_fields(custom_field, update=1)

View File

@@ -0,0 +1,76 @@
from typing import List, NewType
import frappe
StockEntryCode = NewType("StockEntryCode", str)
def execute():
stock_entry_codes = find_broken_stock_entries()
for stock_entry_code in stock_entry_codes:
patched_stock_entry = patch_additional_cost(stock_entry_code)
create_repost_item_valuation(patched_stock_entry)
def find_broken_stock_entries() -> List[StockEntryCode]:
period_closing_date = frappe.db.get_value(
"Period Closing Voucher", {"docstatus": 1}, "posting_date", order_by="posting_date desc"
)
stock_entries_to_patch = frappe.db.sql(
"""
select se.name, sum(sed.additional_cost) as item_additional_cost, se.total_additional_costs
from `tabStock Entry` se
join `tabStock Entry Detail` sed
on sed.parent = se.name
where
se.docstatus = 1 and
se.posting_date > %s
group by
sed.parent
having
item_additional_cost != se.total_additional_costs
""",
period_closing_date,
as_dict=True,
)
return [d.name for d in stock_entries_to_patch]
def patch_additional_cost(code: StockEntryCode):
stock_entry = frappe.get_doc("Stock Entry", code)
stock_entry.distribute_additional_costs()
stock_entry.update_valuation_rate()
stock_entry.set_total_incoming_outgoing_value()
stock_entry.set_total_amount()
stock_entry.db_update()
for item in stock_entry.items:
item.db_update()
return stock_entry
def create_repost_item_valuation(stock_entry):
from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
# turn on recalculate flag so reposting corrects the incoming/outgoing rates.
frappe.db.set_value(
"Stock Ledger Entry",
{"voucher_no": stock_entry.name, "actual_qty": (">", 0)},
"recalculate_rate",
1,
update_modified=False,
)
create_repost_item_valuation_entry(
args=frappe._dict(
{
"posting_date": stock_entry.posting_date,
"posting_time": stock_entry.posting_time,
"voucher_type": stock_entry.doctype,
"voucher_no": stock_entry.name,
"company": stock_entry.company,
}
)
)

View File

@@ -0,0 +1,44 @@
from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
def execute():
company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
if not company:
return
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
fieldtype='Data', fetch_from='item_code.gst_hsn_code', insert_after='description',
allow_on_submit=1, print_hide=1, fetch_if_empty=1)
nil_rated_exempt = dict(fieldname='is_nil_exempt', label='Is Nil Rated or Exempted',
fieldtype='Check', fetch_from='item_code.is_nil_exempt', insert_after='gst_hsn_code',
print_hide=1)
is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST',
fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt',
print_hide=1)
taxable_value = dict(fieldname='taxable_value', label='Taxable Value',
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
print_hide=1)
sales_invoice_gst_fields = [
dict(fieldname='billing_address_gstin', label='Billing Address GSTIN',
fieldtype='Data', insert_after='customer_address', read_only=1,
fetch_from='customer_address.gstin', print_hide=1),
dict(fieldname='customer_gstin', label='Customer GSTIN',
fieldtype='Data', insert_after='shipping_address_name',
fetch_from='shipping_address_name.gstin', print_hide=1),
dict(fieldname='place_of_supply', label='Place of Supply',
fieldtype='Data', insert_after='customer_gstin',
print_hide=1, read_only=1),
dict(fieldname='company_gstin', label='Company GSTIN',
fieldtype='Data', insert_after='company_address',
fetch_from='company_address.gstin', print_hide=1, read_only=1),
]
custom_fields = {
'POS Invoice': sales_invoice_gst_fields,
'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
}
create_custom_fields(custom_fields, update=True)

View File

@@ -0,0 +1,49 @@
from __future__ import unicode_literals
import json
import frappe
def execute():
frappe.reload_doc('accounts', 'doctype', 'purchase_invoice_advance')
frappe.reload_doc('accounts', 'doctype', 'sales_invoice_advance')
purchase_invoices = frappe.db.sql("""
select
parenttype as type, parent as name
from
`tabPurchase Invoice Advance`
where
ref_exchange_rate = 1
and docstatus = 1
and ifnull(exchange_gain_loss, '') != ''
group by
parent
""", as_dict=1)
sales_invoices = frappe.db.sql("""
select
parenttype as type, parent as name
from
`tabSales Invoice Advance`
where
ref_exchange_rate = 1
and docstatus = 1
and ifnull(exchange_gain_loss, '') != ''
group by
parent
""", as_dict=1)
if purchase_invoices + sales_invoices:
frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
for invoice in purchase_invoices + sales_invoices:
doc = frappe.get_doc(invoice.type, invoice.name)
doc.docstatus = 2
doc.make_gl_entries()
for advance in doc.advances:
if advance.ref_exchange_rate == 1:
advance.db_set('exchange_gain_loss', 0, False)
doc.docstatus = 1
doc.make_gl_entries()

View File

@@ -0,0 +1,10 @@
import frappe
def execute():
frappe.reload_doc("maintenance", "doctype", "Maintenance Schedule Detail")
frappe.db.sql("""
UPDATE `tabMaintenance Schedule Detail`
SET completion_status = 'Pending'
WHERE docstatus < 2
""")

View File

@@ -144,6 +144,9 @@ class Project(Document):
if self.sales_order:
frappe.db.set_value("Sales Order", self.sales_order, "project", self.name)
def on_trash(self):
frappe.db.set_value("Sales Order", {"project": self.name}, "project", "")
def update_percent_complete(self):
if self.percent_complete_method == "Manual":
if self.status == "Completed":

View File

@@ -9,6 +9,8 @@ from frappe.utils import add_days, getdate, nowdate
from erpnext.projects.doctype.project_template.test_project_template import make_project_template
from erpnext.projects.doctype.task.test_task import create_task
from erpnext.selling.doctype.sales_order.sales_order import make_project as make_project_from_so
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
@@ -96,6 +98,21 @@ class TestProject(unittest.TestCase):
self.assertEqual(len(tasks), 2)
def test_project_linking_with_sales_order(self):
so = make_sales_order()
project = make_project_from_so(so.name)
project.save()
self.assertEqual(project.sales_order, so.name)
so.reload()
self.assertEqual(so.project, project.name)
project.delete()
so.reload()
self.assertFalse(so.project)
def get_project(name, template):
project = frappe.get_doc(dict(

View File

@@ -345,26 +345,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
}
scan_barcode() {
let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
let show_description = function(idx, exist = null) {
if (exist) {
frappe.show_alert({
message: __('Row #{0}: Qty increased by 1', [idx]),
indicator: 'green'
});
} else {
frappe.show_alert({
message: __('Row #{0}: Item added', [idx]),
indicator: 'green'
});
}
}
let me = this;
if(this.frm.doc.scan_barcode) {
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.search_for_serial_or_batch_or_barcode_number",
args: { search_value: this.frm.doc.scan_barcode }
args: {
search_value: this.frm.doc.scan_barcode
}
}).then(r => {
const data = r && r.message;
if (!data || Object.keys(data).length === 0) {
@@ -375,49 +363,96 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
return;
}
let cur_grid = this.frm.fields_dict.items.grid;
let row_to_modify = null;
const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
if (existing_item_row) {
row_to_modify = existing_item_row;
} else if (blank_item_row) {
row_to_modify = blank_item_row;
}
if (!row_to_modify) {
// add new row
row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
}
show_description(row_to_modify.idx, row_to_modify.item_code);
this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
item_code: data.item_code,
qty: (row_to_modify.qty || 0) + 1
});
['serial_no', 'batch_no', 'barcode'].forEach(field => {
if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
let value = (row_to_modify[field] && field === "serial_no")
? row_to_modify[field] + '\n' + data[field] : data[field];
frappe.model.set_value(row_to_modify.doctype,
row_to_modify.name, field, value);
}
});
scan_barcode_field.set_value('');
refresh_field("items");
me.modify_table_after_scan(data);
});
}
return false;
}
modify_table_after_scan(data) {
let scan_barcode_field = this.frm.fields_dict["scan_barcode"];
let cur_grid = this.frm.fields_dict.items.grid;
let row_to_modify = null;
// Check if batch is scanned and table has batch no field
let batch_no_scan = Boolean(data.batch_no) && frappe.meta.has_field(cur_grid.doctype, "batch_no");
if (batch_no_scan) {
row_to_modify = this.get_batch_row_to_modify(data.batch_no);
} else {
// serial or barcode scan
row_to_modify = this.get_row_to_modify_on_scan(row_to_modify, data);
}
if (!row_to_modify) {
// add new row if new item/batch is scanned
row_to_modify = frappe.model.add_child(this.frm.doc, cur_grid.doctype, 'items');
}
this.show_scan_message(row_to_modify.idx, row_to_modify.item_code);
this.set_scanned_values(row_to_modify, data, scan_barcode_field);
}
set_scanned_values(row_to_modify, data, scan_barcode_field) {
// increase qty and set scanned value and item in row
this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
item_code: data.item_code,
qty: (row_to_modify.qty || 0) + 1
});
['serial_no', 'batch_no', 'barcode'].forEach(field => {
if (data[field] && frappe.meta.has_field(row_to_modify.doctype, field)) {
let is_serial_no = row_to_modify[field] && field === "serial_no";
let value = data[field];
if (is_serial_no) {
value = row_to_modify[field] + '\n' + data[field];
}
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, field, value);
}
});
scan_barcode_field.set_value('');
refresh_field("items");
}
get_row_to_modify_on_scan(row_to_modify, data) {
// get an existing item row to increment or blank row to modify
const existing_item_row = this.frm.doc.items.find(d => d.item_code === data.item_code);
const blank_item_row = this.frm.doc.items.find(d => !d.item_code);
if (existing_item_row) {
row_to_modify = existing_item_row;
} else if (blank_item_row) {
row_to_modify = blank_item_row;
}
return row_to_modify;
}
get_batch_row_to_modify(batch_no) {
// get row if batch already exists in table
const existing_batch_row = this.frm.doc.items.find(d => d.batch_no === batch_no);
return existing_batch_row || null;
}
show_scan_message (idx, exist = null) {
// show new row or qty increase toast
if (exist) {
frappe.show_alert({
message: __('Row #{0}: Qty increased by 1', [idx]),
indicator: 'green'
});
} else {
frappe.show_alert({
message: __('Row #{0}: Item added', [idx]),
indicator: 'green'
});
}
}
apply_default_taxes() {
var me = this;
var taxes_and_charges_field = frappe.meta.get_docfield(me.frm.doc.doctype, "taxes_and_charges",
@@ -617,6 +652,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
me.frm.script_manager.trigger('qty', item.doctype, item.name);
if (!me.frm.doc.set_warehouse)
me.frm.script_manager.trigger('warehouse', item.doctype, item.name);
me.apply_price_list(item, true);
}, undefined, !frappe.flags.hide_serial_batch_dialog);
}
},
@@ -884,7 +920,9 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
if (r.message) {
me.frm.set_value("billing_address", r.message);
} else {
me.frm.set_value("company_address", "");
if (frappe.meta.get_docfield(me.frm.doctype, 'company_address')) {
me.frm.set_value("company_address", "");
}
}
}
});

View File

@@ -63,7 +63,7 @@ erpnext.HierarchyChart = class {
});
node.parent.append(node_card);
node.$link = $(`#${node.id}`);
node.$link = $(`[id="${node.id}"]`);
}
show() {
@@ -223,7 +223,7 @@ erpnext.HierarchyChart = class {
let node = undefined;
$.each(r.message, (_i, data) => {
if ($(`#${data.id}`).length)
if ($(`[id="${data.id}"]`).length)
return;
node = new me.Node({
@@ -263,7 +263,7 @@ erpnext.HierarchyChart = class {
this.refresh_connectors(node.parent_id);
// rebuild incoming connections
let grandparent = $(`#${node.parent_id}`).attr('data-parent');
let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
this.refresh_connectors(grandparent);
}
@@ -282,7 +282,7 @@ erpnext.HierarchyChart = class {
show_active_path(node) {
// mark node parent on active path
$(`#${node.parent_id}`).addClass('active-path');
$(`[id="${node.parent_id}"]`).addClass('active-path');
}
load_children(node, deep=false) {
@@ -317,7 +317,7 @@ erpnext.HierarchyChart = class {
render_child_nodes(node, child_nodes) {
const last_level = this.$hierarchy.find('.level:last').index();
const current_level = $(`#${node.id}`).parent().parent().parent().index();
const current_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
if (last_level === current_level) {
this.$hierarchy.append(`
@@ -382,7 +382,7 @@ erpnext.HierarchyChart = class {
node.$children = $('<ul class="node-children"></ul>');
const last_level = this.$hierarchy.find('.level:last').index();
const node_level = $(`#${node.id}`).parent().parent().parent().index();
const node_level = $(`[id="${node.id}"]`).parent().parent().parent().index();
if (last_level === node_level) {
this.$hierarchy.append(`
@@ -489,7 +489,7 @@ erpnext.HierarchyChart = class {
set_path_attributes(path, parent_id, child_id) {
path.setAttribute("data-parent", parent_id);
path.setAttribute("data-child", child_id);
const parent = $(`#${parent_id}`);
const parent = $(`[id="${parent_id}"]`);
if (parent.hasClass('active')) {
path.setAttribute("class", "active-connector");
@@ -513,7 +513,7 @@ erpnext.HierarchyChart = class {
}
collapse_previous_level_nodes(node) {
let node_parent = $(`#${node.parent_id}`);
let node_parent = $(`[id="${node.parent_id}"]`);
let previous_level_nodes = node_parent.parent().parent().children('li');
let node_card = undefined;
@@ -545,7 +545,7 @@ erpnext.HierarchyChart = class {
setup_node_click_action(node) {
let me = this;
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
node_element.click(function() {
const is_sibling = me.selected_node.parent_id === node.parent_id;
@@ -563,7 +563,7 @@ erpnext.HierarchyChart = class {
}
setup_edit_node_action(node) {
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
let me = this;
node_element.find('.btn-edit-node').click(function() {
@@ -572,7 +572,7 @@ erpnext.HierarchyChart = class {
}
remove_levels_after_node(node) {
let level = $(`#${node.id}`).parent().parent().parent().index();
let level = $(`[id="${node.id}"]`).parent().parent().parent().index();
level = $('.hierarchy > li:eq('+ level + ')');
level.nextAll('li').remove();
@@ -595,7 +595,7 @@ erpnext.HierarchyChart = class {
const parent = $(path).data('parent');
const child = $(path).data('child');
if ($(`#${parent}`).length && $(`#${child}`).length)
if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
return;
$(path).remove();

View File

@@ -54,7 +54,7 @@ erpnext.HierarchyChartMobile = class {
});
node.parent.append(node_card);
node.$link = $(`#${node.id}`);
node.$link = $(`[id="${node.id}"]`);
node.$link.addClass('mobile-node');
}
@@ -184,7 +184,7 @@ erpnext.HierarchyChartMobile = class {
this.refresh_connectors(node.parent_id, node.id);
// rebuild incoming connections of parent
let grandparent = $(`#${node.parent_id}`).attr('data-parent');
let grandparent = $(`[id="${node.parent_id}"]`).attr('data-parent');
this.refresh_connectors(grandparent, node.parent_id);
}
@@ -221,7 +221,7 @@ erpnext.HierarchyChartMobile = class {
show_active_path(node) {
// mark node parent on active path
$(`#${node.parent_id}`).addClass('active-path');
$(`[id="${node.parent_id}"]`).addClass('active-path');
}
load_children(node) {
@@ -256,7 +256,7 @@ erpnext.HierarchyChartMobile = class {
if (child_nodes) {
$.each(child_nodes, (_i, data) => {
this.add_node(node, data);
$(`#${data.id}`).addClass('active-child');
$(`[id="${data.id}"]`).addClass('active-child');
setTimeout(() => {
this.add_connector(node.id, data.id);
@@ -293,9 +293,9 @@ erpnext.HierarchyChartMobile = class {
let connector = undefined;
if ($(`#${parent_id}`).hasClass('active')) {
if ($(`[id="${parent_id}"]`).hasClass('active')) {
connector = this.get_connector_for_active_node(parent_node, child_node);
} else if ($(`#${parent_id}`).hasClass('active-path')) {
} else if ($(`[id="${parent_id}"]`).hasClass('active-path')) {
connector = this.get_connector_for_collapsed_node(parent_node, child_node);
}
@@ -351,7 +351,7 @@ erpnext.HierarchyChartMobile = class {
set_path_attributes(path, parent_id, child_id) {
path.setAttribute("data-parent", parent_id);
path.setAttribute("data-child", child_id);
const parent = $(`#${parent_id}`);
const parent = $(`[id="${parent_id}"]`);
if (parent.hasClass('active')) {
path.setAttribute("class", "active-connector");
@@ -374,7 +374,7 @@ erpnext.HierarchyChartMobile = class {
setup_node_click_action(node) {
let me = this;
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
node_element.click(function() {
let el = undefined;
@@ -398,7 +398,7 @@ erpnext.HierarchyChartMobile = class {
}
setup_edit_node_action(node) {
let node_element = $(`#${node.id}`);
let node_element = $(`[id="${node.id}"]`);
let me = this;
node_element.find('.btn-edit-node').click(function() {
@@ -512,7 +512,7 @@ erpnext.HierarchyChartMobile = class {
}
remove_levels_after_node(node) {
let level = $(`#${node.id}`).parent().parent().index();
let level = $(`[id="${node.id}"]`).parent().parent().index();
level = $('.hierarchy-mobile > li:eq('+ level + ')');
level.nextAll('li').remove();
@@ -533,7 +533,7 @@ erpnext.HierarchyChartMobile = class {
const parent = $(path).data('parent');
const child = $(path).data('child');
if ($(`#${parent}`).length && $(`#${child}`).length)
if ($(`[id="${parent}"]`).length && $(`[id="${child}"]`).length)
return;
$(path).remove();

View File

@@ -714,12 +714,15 @@ erpnext.utils.map_current_doc = function(opts) {
child_columns: opts.child_columns,
action: function(selections, args) {
let values = selections;
if(values.length === 0){
if (values.length === 0) {
frappe.msgprint(__("Please select {0}", [opts.source_doctype]))
return;
}
opts.source_name = values;
opts.args = args;
if (opts.allow_child_item_selection) {
// args contains filtered child docnames
opts.args = args;
}
d.dialog.hide();
_map();
},

View File

@@ -483,6 +483,7 @@ def make_custom_fields(update=True):
'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields,
'POS Invoice': sales_invoice_gst_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
'Payment Entry': payment_entry_fields,
'Journal Entry': journal_entry_fields,
@@ -501,6 +502,7 @@ def make_custom_fields(update=True):
'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'POS Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
@@ -661,6 +663,15 @@ def make_custom_fields(update=True):
'fieldtype': 'Data',
'insert_after': 'email'
}
],
'Finance Book': [
{
'fieldname': 'for_income_tax',
'label': 'For Income Tax',
'fieldtype': 'Check',
'insert_after': 'finance_book_name',
'description': 'If the asset is put to use for less than 180 days, the first Depreciation Rate will be reduced by 50%.'
}
]
}
create_custom_fields(custom_fields, update=update)
@@ -750,7 +761,7 @@ def set_salary_components(docs):
def set_tax_withholding_category(company):
accounts = []
fiscal_year = None
fiscal_year_details = None
abbr = frappe.get_value("Company", company, "abbr")
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')

View File

@@ -112,12 +112,9 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
if frappe.get_meta(item_doctype).has_field('gst_hsn_code'):
return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
else:
return [_("Item"), _("Taxable Amount")] + tax_accounts
return [_("Item"), _("Taxable Amount")] + tax_accounts
def get_itemised_tax_breakup_data(doc, account_wise=False):
def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise)
itemised_taxable_amount = get_itemised_taxable_amount(doc.items)
@@ -125,28 +122,29 @@ def get_itemised_tax_breakup_data(doc, account_wise=False):
if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'):
return itemised_tax, itemised_taxable_amount
item_hsn_map = frappe._dict()
for d in doc.items:
item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code"))
if hsn_wise:
item_hsn_map = frappe._dict()
for d in doc.items:
item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code"))
hsn_tax = {}
for item, taxes in itemised_tax.items():
hsn_code = item_hsn_map.get(item)
hsn_tax.setdefault(hsn_code, frappe._dict())
item_or_hsn = item if not hsn_wise else item_hsn_map.get(item)
hsn_tax.setdefault(item_or_hsn, frappe._dict())
for tax_desc, tax_detail in taxes.items():
key = tax_desc
if account_wise:
key = tax_detail.get('tax_account')
hsn_tax[hsn_code].setdefault(key, {"tax_rate": 0, "tax_amount": 0})
hsn_tax[hsn_code][key]["tax_rate"] = tax_detail.get("tax_rate")
hsn_tax[hsn_code][key]["tax_amount"] += tax_detail.get("tax_amount")
hsn_tax[item_or_hsn].setdefault(key, {"tax_rate": 0, "tax_amount": 0})
hsn_tax[item_or_hsn][key]["tax_rate"] = tax_detail.get("tax_rate")
hsn_tax[item_or_hsn][key]["tax_amount"] += tax_detail.get("tax_amount")
# set taxable amount
hsn_taxable_amount = frappe._dict()
for item in itemised_taxable_amount:
hsn_code = item_hsn_map.get(item)
hsn_taxable_amount.setdefault(hsn_code, 0)
hsn_taxable_amount[hsn_code] += itemised_taxable_amount.get(item)
item_or_hsn = item if not hsn_wise else item_hsn_map.get(item)
hsn_taxable_amount.setdefault(item_or_hsn, 0)
hsn_taxable_amount[item_or_hsn] += itemised_taxable_amount.get(item)
return hsn_tax, hsn_taxable_amount
@@ -440,7 +438,7 @@ def get_ewb_data(dt, dn):
data.itemList = []
data.totalValue = doc.total
data = get_item_list(data, doc)
data = get_item_list(data, doc, hsn_wise=True)
disable_rounded = frappe.db.get_single_value('Global Defaults', 'disable_rounded_total')
data.totInvValue = doc.grand_total if disable_rounded else doc.rounded_total
@@ -551,7 +549,7 @@ def get_address_details(data, doc, company_address, billing_address, dispatch_ad
return data
def get_item_list(data, doc):
def get_item_list(data, doc, hsn_wise=False):
for attr in ['cgstValue', 'sgstValue', 'igstValue', 'cessValue', 'OthValue']:
data[attr] = 0
@@ -563,7 +561,7 @@ def get_item_list(data, doc):
'cess_account': ['cessRate', 'cessValue']
}
item_data_attrs = ['sgstRate', 'cgstRate', 'igstRate', 'cessRate', 'cessNonAdvol']
hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True)
hsn_wise_charges, hsn_taxable_amount = get_itemised_tax_breakup_data(doc, account_wise=True, hsn_wise=hsn_wise)
for hsn_code, taxable_amount in hsn_taxable_amount.items():
item_data = frappe._dict()
if not hsn_code:
@@ -859,12 +857,13 @@ def get_depreciation_amount(asset, depreciable_value, row):
rate_of_depreciation = row.rate_of_depreciation
# if its the first depreciation
if depreciable_value == asset.gross_purchase_amount:
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
if diff <= 180:
rate_of_depreciation = rate_of_depreciation / 2
frappe.msgprint(
_('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
if row.finance_book and frappe.db.get_value('Finance Book', row.finance_book, 'for_income_tax'):
# as per IT act, if the asset is purchased in the 2nd half of fiscal year, then rate is divided by 2
diff = date_diff(row.depreciation_start_date, asset.available_for_use_date)
if diff <= 180:
rate_of_depreciation = rate_of_depreciation / 2
frappe.msgprint(
_('As per IT Act, the rate of depreciation for the first depreciation entry is reduced by 50%.'))
depreciation_amount = flt(depreciable_value * (flt(rate_of_depreciation) / 100))

View File

@@ -1480,6 +1480,7 @@
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1512,7 +1513,7 @@
"idx": 105,
"is_submittable": 1,
"links": [],
"modified": "2021-09-01 15:12:24.115483",
"modified": "2021-09-28 13:09:51.515542",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",

View File

@@ -236,7 +236,7 @@ erpnext.PointOfSale.ItemDetails = class {
if (this.value) {
me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => {
me.item_stock_map = me.events.get_item_stock_map();
const available_qty = me.item_stock_map[me.item_row.item_code][this.value];
const available_qty = me.item_stock_map[me.item_row.item_code] && me.item_stock_map[me.item_row.item_code][this.value];
if (available_qty === undefined) {
me.events.get_available_stock(me.item_row.item_code, this.value).then(() => {
// item stock map is updated now reset warehouse

View File

@@ -2116,9 +2116,9 @@
},
"Saudi Arabia": {
"KSA VAT 5%": {
"account_name": "VAT 5%",
"tax_rate": 5.00
"KSA VAT 15%": {
"account_name": "VAT 15%",
"tax_rate": 15.00
},
"KSA VAT Zero": {
"account_name": "VAT Zero",

View File

@@ -1277,6 +1277,7 @@
"fetch_from": "customer.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1308,7 +1309,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
"modified": "2021-08-27 20:14:40.215231",
"modified": "2021-09-28 13:10:09.761714",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",

View File

@@ -563,8 +563,12 @@ class Item(WebsiteGenerator):
_("Default BOM ({0}) must be active for this item or its template").format(bom_item))
def fill_customer_code(self):
""" Append all the customer codes and insert into "customer_code" field of item table """
self.customer_code = ','.join(d.ref_code for d in self.get("customer_items", []))
"""
Append all the customer codes and insert into "customer_code" field of item table.
Used to search Item by customer code.
"""
customer_codes = set(d.ref_code for d in self.get("customer_items", []))
self.customer_code = ','.join(customer_codes)
def check_item_tax(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""

View File

@@ -272,8 +272,9 @@ def update_status(name, status):
material_request.update_status(status)
@frappe.whitelist()
def make_purchase_order(source_name, target_doc=None, args={}):
def make_purchase_order(source_name, target_doc=None, args=None):
if args is None:
args = {}
if isinstance(args, string_types):
args = json.loads(args)

View File

@@ -36,7 +36,8 @@
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty"
"label": "Qty",
"reqd": 1
},
{
"fieldname": "picked_qty",
@@ -180,7 +181,7 @@
],
"istable": 1,
"links": [],
"modified": "2020-06-24 17:18:57.357120",
"modified": "2021-09-28 12:02:16.923056",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@@ -1140,6 +1140,7 @@
"fetch_from": "supplier.represents_company",
"fieldname": "represents_company",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
"options": "Company",
"read_only": 1
@@ -1149,7 +1150,7 @@
"idx": 261,
"is_submittable": 1,
"links": [],
"modified": "2021-08-17 20:16:40.849885",
"modified": "2021-09-28 13:11:10.181328",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",

View File

@@ -548,44 +548,7 @@ frappe.ui.form.on('Stock Entry', {
calculate_basic_amount: function(frm, item) {
item.basic_amount = flt(flt(item.transfer_qty) * flt(item.basic_rate),
precision("basic_amount", item));
frm.events.calculate_amount(frm);
},
calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
let total_basic_amount = 0;
if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
total_basic_amount = frappe.utils.sum(
(frm.doc.items || []).map(function(i) {
return i.is_finished_item ? flt(i.basic_amount) : 0;
})
);
} else {
total_basic_amount = frappe.utils.sum(
(frm.doc.items || []).map(function(i) {
return i.t_warehouse ? flt(i.basic_amount) : 0;
})
);
}
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
} else {
item.additional_cost = 0;
}
item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
if (flt(item.transfer_qty)) {
item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
precision("valuation_rate", item));
}
}
refresh_field('items');
},
calculate_total_additional_costs: function(frm) {
@@ -781,11 +744,6 @@ frappe.ui.form.on('Landed Cost Taxes and Charges', {
amount: function(frm, cdt, cdn) {
frm.events.set_base_amount(frm, cdt, cdn);
// Adding this check because same table in used in LCV
// This causes an error if you try to post an LCV immediately after a Stock Entry
if (frm.doc.doctype == 'Stock Entry') {
frm.events.calculate_amount(frm);
}
},
expense_account: function(frm, cdt, cdn) {
@@ -1100,4 +1058,4 @@ function check_should_not_attach_bom_items(bom_no) {
);
}
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));
extend_cscript(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));

View File

@@ -555,22 +555,27 @@ class StockEntry(StockController):
def distribute_additional_costs(self):
# If no incoming items, set additional costs blank
if not any([d.item_code for d in self.items if d.t_warehouse]):
if not any(d.item_code for d in self.items if d.t_warehouse):
self.additional_costs = []
self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")])
self.total_additional_costs = sum(flt(t.base_amount) for t in self.get("additional_costs"))
if self.purpose in ("Repack", "Manufacture"):
incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
incoming_items_cost = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
if incoming_items_cost:
for d in self.get("items"):
if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
else:
d.additional_cost = 0
if not incoming_items_cost:
return
for d in self.get("items"):
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
d.additional_cost = 0
continue
elif not d.t_warehouse:
d.additional_cost = 0
continue
d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
def update_valuation_rate(self):
for d in self.get("items"):
@@ -805,7 +810,11 @@ class StockEntry(StockController):
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
if self.purpose in ("Repack", "Manufacture"):
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.is_finished_item)
else:
total_basic_amount = sum(flt(t.basic_amount) for t in self.get("items") if t.t_warehouse)
divide_based_on = total_basic_amount
if self.get("additional_costs") and not total_basic_amount:
@@ -816,20 +825,24 @@ class StockEntry(StockController):
for t in self.get("additional_costs"):
for d in self.get("items"):
if d.t_warehouse:
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
"amount": 0.0,
"base_amount": 0.0
})
if self.purpose in ("Repack", "Manufacture") and not d.is_finished_item:
continue
elif not d.t_warehouse:
continue
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
"amount": 0.0,
"base_amount": 0.0
})
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
flt(t.amount * multiply_based_on) / divide_based_on
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
flt(t.base_amount * multiply_based_on) / divide_based_on
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
flt(t.amount * multiply_based_on) / divide_based_on
item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
flt(t.base_amount * multiply_based_on) / divide_based_on
if item_account_wise_additional_cost:
for d in self.get("items"):

View File

@@ -837,6 +837,39 @@ class TestStockEntry(unittest.TestCase):
frappe.db.set_default("allow_negative_stock", 0)
def test_additional_cost_distribution_manufacture(self):
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Manufacture",
additional_costs=[frappe._dict(base_amount=100)],
items=[
frappe._dict(item_code="RM", basic_amount=10),
frappe._dict(item_code="FG", basic_amount=20, t_warehouse="X", is_finished_item=1),
frappe._dict(item_code="scrap", basic_amount=30, t_warehouse="X")
],
)
se.distribute_additional_costs()
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([0.0, 100.0, 0.0], distributed_costs)
def test_additional_cost_distribution_non_manufacture(self):
se = frappe.get_doc(
doctype="Stock Entry",
purpose="Material Receipt",
additional_costs=[frappe._dict(base_amount=100)],
items=[
frappe._dict(item_code="RECEIVED_1", basic_amount=20, t_warehouse="X"),
frappe._dict(item_code="RECEIVED_2", basic_amount=30, t_warehouse="X")
],
)
se.distribute_additional_costs()
distributed_costs = [d.additional_cost for d in se.items]
self.assertEqual([40.0, 60.0], distributed_costs)
def make_serialized_item(**args):
args = frappe._dict(args)
se = frappe.copy_doc(test_records[0])

View File

@@ -11,6 +11,6 @@
{% endfor %}
</ol>
{% else %}
<p>You don't have no upcoming holidays this {{ frequency }}.</p>
<p>You have no upcoming holidays this {{ frequency }}.</p>
{% endif %}
{% endif %}

View File

@@ -7,7 +7,7 @@
</div>
{% else %}
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
<label>{{ _(doc.meta.get_label('total')) }}</label></div>
<label>{{ _(df.label) }}</label></div>
<div class="col-xs-7 text-right">
{{ doc.get_formatted("total", doc) }}
</div>

View File

@@ -7,6 +7,8 @@ def create_employee_records():
create_company()
create_missing_designation()
frappe.db.sql("DELETE FROM tabEmployee WHERE company='Test Org Chart'")
emp1 = create_employee('Test Employee 1', 'CEO')
emp2 = create_employee('Test Employee 2', 'CTO')
emp3 = create_employee('Test Employee 3', 'Head of Marketing and Sales', emp1)