Merge branch 'develop' into skip_tcs

This commit is contained in:
Deepesh Garg 2023-07-22 18:38:45 +05:30 committed by GitHub
commit 6b2dbdd394
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
121 changed files with 2751 additions and 2599 deletions

38
.github/workflows/release_notes.yml vendored Normal file
View File

@ -0,0 +1,38 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
# This action needs to be maintained on all branches that do releases.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v13.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}

View File

@ -7,11 +7,9 @@ on:
- '**.css' - '**.css'
- '**.md' - '**.md'
- '**.html' - '**.html'
push: schedule:
branches: [ develop ] # Run everday at midnight UTC / 5:30 IST
paths-ignore: - cron: "0 0 * * *"
- '**.js'
- '**.md'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
user: user:

View File

@ -4,18 +4,19 @@
"creation": "2020-07-17 11:25:34.593061", "creation": "2020-07-17 11:25:34.593061",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}", "filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 12:24:49.144210", "modified": "2023-07-19 13:13:13.307073",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget Variance", "name": "Budget Variance",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Budget Variance Report", "report_name": "Budget Variance Report",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -4,18 +4,19 @@
"creation": "2020-07-17 11:25:34.448572", "creation": "2020-07-17 11:25:34.448572",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}", "filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 12:33:48.888943", "modified": "2023-07-19 13:08:56.470390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Profit and Loss", "name": "Profit and Loss",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Profit and Loss Statement", "report_name": "Profit and Loss Statement",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -14,10 +14,8 @@ class AccountClosingBalance(Document):
pass pass
def make_closing_entries(closing_entries, voucher_name): def make_closing_entries(closing_entries, voucher_name, company, closing_date):
accounting_dimensions = get_accounting_dimensions() accounting_dimensions = get_accounting_dimensions()
company = closing_entries[0].get("company")
closing_date = closing_entries[0].get("closing_date")
previous_closing_entries = get_previous_closing_entries( previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions company, closing_date, accounting_dimensions

View File

@ -271,6 +271,12 @@ def get_dimensions(with_cost_center_and_project=False):
as_dict=1, as_dict=1,
) )
if isinstance(with_cost_center_and_project, str):
if with_cost_center_and_project.lower().strip() == "true":
with_cost_center_and_project = True
else:
with_cost_center_and_project = False
if with_cost_center_and_project: if with_cost_center_and_project:
dimension_filters.extend( dimension_filters.extend(
[ [

View File

@ -20,5 +20,11 @@ frappe.ui.form.on('Accounting Period', {
} }
}); });
} }
frm.set_query("document_type", "closed_documents", () => {
return {
query: "erpnext.controllers.queries.get_doctypes_for_closing",
}
});
} }
}); });

View File

@ -11,6 +11,10 @@ class OverlapError(frappe.ValidationError):
pass pass
class ClosedAccountingPeriod(frappe.ValidationError):
pass
class AccountingPeriod(Document): class AccountingPeriod(Document):
def validate(self): def validate(self):
self.validate_overlap() self.validate_overlap()
@ -65,3 +69,42 @@ class AccountingPeriod(Document):
"closed_documents", "closed_documents",
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
) )
def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance":
return
elif doc.doctype == "Asset":
if doc.is_existing_asset:
return
else:
date = doc.available_for_use_date
elif doc.doctype == "Asset Repair":
date = doc.completion_date
else:
date = doc.posting_date
ap = frappe.qb.DocType("Accounting Period")
cd = frappe.qb.DocType("Closed Document")
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
& (cd.closed == 1)
& (cd.document_type == doc.doctype)
& (date >= ap.start_date)
& (date <= ap.end_date)
)
).run(as_dict=1)
if accounting_period:
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])
),
ClosedAccountingPeriod,
)

View File

@ -6,9 +6,11 @@ import unittest
import frappe import frappe
from frappe.utils import add_months, nowdate from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError from erpnext.accounts.doctype.accounting_period.accounting_period import (
ClosedAccountingPeriod,
OverlapError,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ["Item"] test_dependencies = ["Item"]
@ -33,9 +35,9 @@ class TestAccountingPeriod(unittest.TestCase):
ap1.save() ap1.save()
doc = create_sales_invoice( doc = create_sales_invoice(
do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
) )
self.assertRaises(ClosedAccountingPeriod, doc.submit) self.assertRaises(ClosedAccountingPeriod, doc.save)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Accounting Period"): for d in frappe.get_all("Accounting Period"):

View File

@ -8,9 +8,6 @@ frappe.ui.form.on('Bank', {
}, },
refresh: function(frm) { refresh: function(frm) {
add_fields_to_mapping_table(frm); add_fields_to_mapping_table(frm);
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank' };
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
if (frm.doc.__islocal) { if (frm.doc.__islocal) {

View File

@ -1,13 +1,14 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Dunning", { frappe.ui.form.on("Dunning", {
setup: function (frm) { setup: function (frm) {
frm.set_query("sales_invoice", () => { frm.set_query("sales_invoice", "overdue_payments", () => {
return { return {
filters: { filters: {
docstatus: 1, docstatus: 1,
company: frm.doc.company, company: frm.doc.company,
customer: frm.doc.customer,
outstanding_amount: [">", 0], outstanding_amount: [">", 0],
status: "Overdue" status: "Overdue"
}, },
@ -22,14 +23,24 @@ frappe.ui.form.on("Dunning", {
} }
}; };
}); });
frm.set_query("cost_center", () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query("contact_person", erpnext.queries.contact_query);
frm.set_query("customer_address", erpnext.queries.address_query);
frm.set_query("company_address", erpnext.queries.company_address_query);
// cannot add rows manually, only via button "Fetch Overdue Payments"
frm.set_df_property("overdue_payments", "cannot_add_rows", true);
}, },
refresh: function (frm) { refresh: function (frm) {
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
frm.set_df_property(
"sales_invoice",
"read_only",
frm.doc.__islocal ? 0 : 1
);
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") { if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
frm.add_custom_button(__("Resolve"), () => { frm.add_custom_button(__("Resolve"), () => {
frm.set_value("status", "Resolved"); frm.set_value("status", "Resolved");
@ -40,42 +51,111 @@ frappe.ui.form.on("Dunning", {
__("Payment"), __("Payment"),
function () { function () {
frm.events.make_payment_entry(frm); frm.events.make_payment_entry(frm);
},__("Create") }, __("Create")
); );
frm.page.set_inner_btn_group_as_primary(__("Create")); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
if(frm.doc.docstatus > 0) { if (frm.doc.docstatus === 0) {
frm.add_custom_button(__('Ledger'), function() { frm.add_custom_button(__("Fetch Overdue Payments"), () => {
frappe.route_options = { erpnext.utils.map_current_doc({
"voucher_no": frm.doc.name, method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
"from_date": frm.doc.posting_date, source_doctype: "Sales Invoice",
"to_date": frm.doc.posting_date, date_field: "due_date",
"company": frm.doc.company, target: frm,
"show_cancelled_entries": frm.doc.docstatus === 2 setters: {
}; customer: frm.doc.customer || undefined,
frappe.set_route("query-report", "General Ledger"); },
}, __('View')); get_query_filters: {
docstatus: 1,
status: "Overdue",
company: frm.doc.company
},
allow_child_item_selection: true,
child_fieldname: "payment_schedule",
child_columns: ["due_date", "outstanding"],
});
});
} }
frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
}, },
overdue_days: function (frm) { // When multiple companies are set up. in case company name is changed set default company address
frappe.db.get_value( company: function (frm) {
"Dunning Type", if (frm.doc.company) {
{ frappe.call({
start_day: ["<", frm.doc.overdue_days], method: "erpnext.setup.doctype.company.company.get_default_company_address",
end_day: [">=", frm.doc.overdue_days], args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
}, debounce: 2000,
"dunning_type", callback: function (r) {
(r) => { frm.set_value("company_address", r && r.message || "");
if (r) { }
frm.set_value("dunning_type", r.dunning_type); });
} else {
frm.set_value("dunning_type", ""); if (frm.fields_dict.currency) {
frm.set_value("rate_of_interest", ""); const company_currency = erpnext.get_currency(frm.doc.company);
frm.set_value("dunning_fee", "");
if (!frm.doc.currency) {
frm.set_value("currency", company_currency);
}
if (frm.doc.currency == company_currency) {
frm.set_value("conversion_rate", 1.0);
} }
} }
);
const company_doc = frappe.get_doc(":Company", frm.doc.company);
if (company_doc.default_letter_head) {
if (frm.fields_dict.letter_head) {
frm.set_value("letter_head", company_doc.default_letter_head);
}
}
}
},
currency: function (frm) {
// this.set_dynamic_labels();
const company_currency = erpnext.get_currency(frm.doc.company);
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc
if (frm.doc.currency && frm.doc.currency !== company_currency) {
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
transaction_date: frm.doc.posting_date,
from_currency: frm.doc.currency,
to_currency: company_currency,
args: "for_selling"
},
freeze: true,
freeze_message: __("Fetching exchange rates ..."),
callback: function(r) {
const exchange_rate = flt(r.message);
if (exchange_rate != frm.doc.conversion_rate) {
frm.set_value("conversion_rate", exchange_rate);
}
}
});
} else {
frm.trigger("conversion_rate");
}
},
customer: (frm) => {
erpnext.utils.get_party_details(frm);
},
conversion_rate: function (frm) {
if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
frm.set_value("conversion_rate", 1.0);
}
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
customer_address: function (frm) {
erpnext.utils.get_address_display(frm, "customer_address");
},
company_address: function (frm) {
erpnext.utils.get_address_display(frm, "company_address");
}, },
dunning_type: function (frm) { dunning_type: function (frm) {
frm.trigger("get_dunning_letter_text"); frm.trigger("get_dunning_letter_text");
@ -87,7 +167,7 @@ frappe.ui.form.on("Dunning", {
if (frm.doc.dunning_type) { if (frm.doc.dunning_type) {
frappe.call({ frappe.call({
method: method:
"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", "erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text",
args: { args: {
dunning_type: frm.doc.dunning_type, dunning_type: frm.doc.dunning_type,
language: frm.doc.language, language: frm.doc.language,
@ -106,49 +186,62 @@ frappe.ui.form.on("Dunning", {
}); });
} }
}, },
due_date: function (frm) {
frm.trigger("calculate_overdue_days");
},
posting_date: function (frm) { posting_date: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_overdue_days");
}, },
rate_of_interest: function (frm) { rate_of_interest: function (frm) {
frm.trigger("calculate_interest_and_amount"); frm.trigger("calculate_interest");
},
outstanding_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
},
interest_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
}, },
dunning_fee: function (frm) { dunning_fee: function (frm) {
frm.trigger("calculate_interest_and_amount"); frm.trigger("calculate_totals");
}, },
sales_invoice: function (frm) { overdue_payments_add: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_totals");
},
overdue_payments_remove: function (frm) {
frm.trigger("calculate_totals");
}, },
calculate_overdue_days: function (frm) { calculate_overdue_days: function (frm) {
if (frm.doc.posting_date && frm.doc.due_date) { frm.doc.overdue_payments.forEach((row) => {
const overdue_days = moment(frm.doc.posting_date).diff( if (frm.doc.posting_date && row.due_date) {
frm.doc.due_date, const overdue_days = moment(frm.doc.posting_date).diff(
"days" row.due_date,
); "days"
frm.set_value("overdue_days", overdue_days); );
} frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days);
}
});
}, },
calculate_interest_and_amount: function (frm) { calculate_interest: function (frm) {
const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; frm.doc.overdue_payments.forEach((row) => {
const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount')); const interest_per_day = frm.doc.rate_of_interest / 100 / 365;
const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount')); const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row));
const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total')); frappe.model.set_value(row.doctype, row.name, "interest", interest);
frm.set_value("interest_amount", interest_amount); });
frm.set_value("dunning_amount", dunning_amount); },
frm.set_value("grand_total", grand_total); calculate_totals: function (frm) {
const total_interest = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.interest, 0);
const total_outstanding = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.outstanding, 0);
const dunning_amount = total_interest + frm.doc.dunning_fee;
const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
const grand_total = total_outstanding + dunning_amount;
function setWithPrecison(field, value) {
frm.set_value(field, flt(value, precision(field)));
}
setWithPrecison("total_outstanding", total_outstanding);
setWithPrecison("total_interest", total_interest);
setWithPrecison("dunning_amount", dunning_amount);
setWithPrecison("base_dunning_amount", base_dunning_amount);
setWithPrecison("grand_total", grand_total);
}, },
make_payment_entry: function (frm) { make_payment_entry: function (frm) {
return frappe.call({ return frappe.call({
method: method:
"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry",
args: { args: {
dt: frm.doc.doctype, dt: frm.doc.doctype,
dn: frm.doc.name, dn: frm.doc.name,
@ -160,3 +253,9 @@ frappe.ui.form.on("Dunning", {
}); });
}, },
}); });
frappe.ui.form.on("Overdue Payment", {
interest: function (frm) {
frm.trigger("calculate_totals");
}
});

View File

@ -2,49 +2,60 @@
"actions": [], "actions": [],
"allow_events_in_timeline": 1, "allow_events_in_timeline": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238", "creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title",
"naming_series", "naming_series",
"sales_invoice",
"customer", "customer",
"customer_name", "customer_name",
"outstanding_amount",
"currency",
"conversion_rate",
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"posting_time", "posting_time",
"due_date", "status",
"overdue_days", "section_break_9",
"currency",
"column_break_11",
"conversion_rate",
"address_and_contact_section", "address_and_contact_section",
"customer_address",
"address_display", "address_display",
"contact_person",
"contact_display", "contact_display",
"column_break_16",
"company_address",
"company_address_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"column_break_18",
"company_address_display",
"section_break_6", "section_break_6",
"dunning_type", "dunning_type",
"dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"interest_amount",
"section_break_12", "section_break_12",
"dunning_amount", "overdue_payments",
"grand_total", "section_break_28",
"income_account", "total_interest",
"dunning_fee",
"column_break_17", "column_break_17",
"status", "dunning_amount",
"printing_setting_section", "base_dunning_amount",
"section_break_32",
"spacer",
"column_break_33",
"total_outstanding",
"grand_total",
"printing_settings_section",
"language", "language",
"body_text", "body_text",
"column_break_22", "column_break_22",
"letter_head", "letter_head",
"closing_text", "closing_text",
"accounting_details_section",
"income_account",
"column_break_48",
"cost_center",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@ -60,32 +71,17 @@
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"options": "DUNN-.MM.-.YY.-" "options": "DUNN-.MM.-.YY.-",
"print_hide": 1
}, },
{ {
"fieldname": "sales_invoice", "fetch_from": "customer.customer_name",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"reqd": 1
},
{
"fetch_from": "sales_invoice.customer_name",
"fieldname": "customer_name", "fieldname": "customer_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Customer Name", "label": "Customer Name",
"read_only": 1 "read_only": 1
}, },
{
"fetch_from": "sales_invoice.outstanding_amount",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"label": "Outstanding Amount",
"read_only": 1
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@ -94,13 +90,8 @@
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Date" "label": "Date",
}, "reqd": 1
{
"fieldname": "overdue_days",
"fieldtype": "Int",
"label": "Overdue Days",
"read_only": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
@ -112,16 +103,7 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Dunning Type", "label": "Dunning Type",
"options": "Dunning Type", "options": "Dunning Type"
"reqd": 1
},
{
"default": "0",
"fieldname": "interest_amount",
"fieldtype": "Currency",
"label": "Interest Amount",
"precision": "2",
"read_only": 1
}, },
{ {
"fieldname": "column_break_8", "fieldname": "column_break_8",
@ -134,6 +116,7 @@
"fieldname": "dunning_fee", "fieldname": "dunning_fee",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Dunning Fee", "label": "Dunning Fee",
"options": "currency",
"precision": "2" "precision": "2"
}, },
{ {
@ -144,36 +127,24 @@
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "printing_setting_section",
"fieldtype": "Section Break",
"label": "Printing Setting"
},
{ {
"fieldname": "language", "fieldname": "language",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Print Language", "label": "Print Language",
"options": "Language" "options": "Language",
"print_hide": 1
}, },
{ {
"fieldname": "letter_head", "fieldname": "letter_head",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Letter Head", "label": "Letter Head",
"options": "Letter Head" "options": "Letter Head",
"print_hide": 1
}, },
{ {
"fieldname": "column_break_22", "fieldname": "column_break_22",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fetch_from": "sales_invoice.currency",
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@ -183,14 +154,6 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title"
},
{ {
"fieldname": "body_text", "fieldname": "body_text",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
@ -201,13 +164,6 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Closing Text" "label": "Closing Text"
}, },
{
"fetch_from": "sales_invoice.due_date",
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{ {
"fieldname": "posting_time", "fieldname": "posting_time",
"fieldtype": "Time", "fieldtype": "Time",
@ -222,26 +178,24 @@
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
}, },
{ {
"collapsible": 1,
"fieldname": "address_and_contact_section", "fieldname": "address_and_contact_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
}, },
{ {
"fetch_from": "sales_invoice.address_display",
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Address", "label": "Address",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_display",
"fieldname": "contact_display", "fieldname": "contact_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact", "label": "Contact",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_mobile",
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
@ -249,18 +203,12 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.company_address_display",
"fieldname": "company_address_display", "fieldname": "company_address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Company Address", "label": "Company Address Display",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_email",
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Contact Email", "label": "Contact Email",
@ -268,18 +216,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.customer",
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Customer", "label": "Customer",
"options": "Customer", "options": "Customer",
"read_only": 1 "reqd": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "grand_total", "fieldname": "grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total", "label": "Grand Total",
"options": "currency",
"precision": "2", "precision": "2",
"read_only": 1 "read_only": 1
}, },
@ -290,33 +238,150 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "Draft\nResolved\nUnresolved\nCancelled" "options": "Draft\nResolved\nUnresolved\nCancelled",
},
{
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Dunning Amount",
"read_only": 1 "read_only": 1
}, },
{ {
"description": "For dunning fee and interest",
"fetch_from": "dunning_type.income_account",
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
"options": "Account" "options": "Account",
"print_hide": 1
},
{
"fieldname": "overdue_payments",
"fieldtype": "Table",
"label": "Overdue Payments",
"options": "Overdue Payment"
},
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "total_interest",
"fieldtype": "Currency",
"label": "Total Interest",
"options": "currency",
"precision": "2",
"read_only": 1
},
{
"fieldname": "total_outstanding",
"fieldtype": "Currency",
"label": "Total Outstanding",
"options": "currency",
"read_only": 1
},
{
"fieldname": "customer_address",
"fieldtype": "Link",
"label": "Customer Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"default": "0",
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount",
"options": "currency",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "dunning_type.cost_center",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "printing_settings_section",
"fieldtype": "Section Break",
"label": "Printing Settings"
},
{
"fieldname": "section_break_32",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "spacer",
"fieldtype": "Data",
"hidden": 1,
"label": "Spacer",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
"label": "Company Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}, },
{ {
"fetch_from": "sales_invoice.conversion_rate",
"fieldname": "conversion_rate", "fieldname": "conversion_rate",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 1, "label": "Conversion Rate"
"label": "Conversion Rate", },
{
"default": "0",
"fieldname": "base_dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_48",
"fieldtype": "Column Break"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-03 16:24:01.677026", "modified": "2023-06-15 15:46:53.865712",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning", "name": "Dunning",

View File

@ -1,131 +1,150 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
"""
# Accounting
1. Payment of outstanding invoices with dunning amount
- Debit full amount to bank
- Credit invoiced amount to receivables
- Credit dunning amount to interest and similar revenue
-> Resolves dunning automatically
"""
import json import json
import frappe import frappe
from frappe.utils import cint, flt, getdate from frappe import _
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class Dunning(AccountsController): class Dunning(AccountsController):
def validate(self): def validate(self):
self.validate_overdue_days() self.validate_same_currency()
self.validate_amount() self.validate_overdue_payments()
if not self.income_account: self.validate_totals()
self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account") self.set_party_details()
self.set_dunning_level()
def validate_overdue_days(self): def validate_same_currency(self):
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 """
Throw an error if invoice currency differs from dunning currency.
"""
for row in self.overdue_payments:
invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency")
if invoice_currency != self.currency:
frappe.throw(
_(
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
).format(row.sales_invoice, invoice_currency, self.currency)
)
def validate_amount(self): def validate_overdue_payments(self):
amounts = calculate_interest_and_amount( daily_interest = self.rate_of_interest / 100 / 365
self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
for row in self.overdue_payments:
row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0
row.interest = row.outstanding * daily_interest * row.overdue_days
def validate_totals(self):
self.total_outstanding = sum(row.outstanding for row in self.overdue_payments)
self.total_interest = sum(row.interest for row in self.overdue_payments)
self.dunning_amount = self.total_interest + self.dunning_fee
self.base_dunning_amount = self.dunning_amount * self.conversion_rate
self.grand_total = self.total_outstanding + self.dunning_amount
def set_party_details(self):
from erpnext.accounts.party import _get_party_details
party_details = _get_party_details(
self.customer,
ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype,
company=self.company,
posting_date=self.get("posting_date"),
fetch_payment_terms_template=False,
party_address=self.customer_address,
company_address=self.get("company_address"),
) )
if self.interest_amount != amounts.get("interest_amount"): for field in [
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount")) "customer_address",
if self.dunning_amount != amounts.get("dunning_amount"): "address_display",
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount")) "company_address",
if self.grand_total != amounts.get("grand_total"): "contact_person",
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total")) "contact_display",
"contact_mobile",
]:
self.set(field, party_details.get(field))
def on_submit(self): self.set("company_address_display", get_address_display(self.company_address))
self.make_gl_entries()
def on_cancel(self): def set_dunning_level(self):
if self.dunning_amount: for row in self.overdue_payments:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry") past_dunnings = frappe.get_all(
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) "Overdue Payment",
filters={
def make_gl_entries(self): "payment_schedule": row.payment_schedule,
if not self.dunning_amount: "parent": ("!=", row.parent),
return "docstatus": 1,
gl_entries = []
invoice_fields = [
"project",
"cost_center",
"debit_to",
"party_account_currency",
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(
self.get_gl_dict(
{
"account": inv.debit_to,
"party_type": "Customer",
"party": self.customer,
"due_date": self.due_date,
"against": self.income_account,
"debit": dunning_in_company_currency,
"debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}, },
inv.party_account_currency,
item=inv,
) )
) row.dunning_level = len(past_dunnings) + 1
gl_entries.append(
self.get_gl_dict(
{
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"cost_center": inv.cost_center or default_cost_center,
"credit_in_account_currency": self.dunning_amount,
"project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)
def resolve_dunning(doc, state): def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references: for reference in doc.references:
if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0: # Consider partial and full payments:
dunnings = frappe.get_list( # Submitting full payment: outstanding_amount will be 0
"Dunning", # Submitting 1st partial payment: outstanding_amount will be the pending installment
filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")}, # Cancelling full payment: outstanding_amount will revert to total amount
ignore_permissions=True, # Cancelling last partial payment: outstanding_amount will revert to pending amount
) submit_condition = reference.outstanding_amount < reference.total_amount
cancel_condition = reference.outstanding_amount <= reference.total_amount
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
for dunning in dunnings: for dunning in dunnings:
frappe.db.set_value("Dunning", dunning.name, "status", "Resolved") resolve = True
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
dunning.status = "Resolved" if resolve else "Unresolved"
dunning.save()
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days): def get_linked_dunnings_as_per_state(sales_invoice, state):
interest_amount = 0 dunning = frappe.qb.DocType("Dunning")
grand_total = flt(outstanding_amount) + flt(dunning_fee) overdue_payment = frappe.qb.DocType("Overdue Payment")
if rate_of_interest:
interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100 return (
interest_amount = (interest_per_year * cint(overdue_days)) / 365 frappe.qb.from_(dunning)
grand_total += flt(interest_amount) .join(overdue_payment)
dunning_amount = flt(interest_amount) + flt(dunning_fee) .on(overdue_payment.parent == dunning.name)
return { .select(dunning.name)
"interest_amount": interest_amount, .where(
"grand_total": grand_total, (dunning.status == state)
"dunning_amount": dunning_amount, & (dunning.docstatus != 2)
} & (overdue_payment.sales_invoice == sales_invoice)
)
).run(as_dict=True)
@frappe.whitelist() @frappe.whitelist()

View File

@ -1,12 +0,0 @@
from frappe import _
def get_data():
return {
"fieldname": "dunning",
"non_standard_fieldnames": {
"Journal Entry": "reference_name",
"Payment Entry": "reference_name",
},
"transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
}

View File

@ -1,162 +1,197 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate, today from frappe.utils import add_days, nowdate, today
from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
unlink_payment_on_cancel_of_invoice, unlink_payment_on_cancel_of_invoice,
) )
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
create_dunning as create_dunning_from_sales_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice_against_cost_center, create_sales_invoice_against_cost_center,
) )
test_dependencies = ["Company", "Cost Center"]
class TestDunning(unittest.TestCase):
class TestDunning(FrappeTestCase):
@classmethod @classmethod
def setUpClass(self): def setUpClass(cls):
create_dunning_type() super().setUpClass()
create_dunning_type_with_zero_interest_rate() create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1)
create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
unlink_payment_on_cancel_of_invoice() unlink_payment_on_cancel_of_invoice()
@classmethod @classmethod
def tearDownClass(self): def tearDownClass(cls):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
super().tearDownClass()
def test_dunning(self): def test_dunning_without_fees(self):
dunning = create_dunning() dunning = create_dunning(overdue_days=20)
amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
)
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
def test_dunning_with_zero_interest_rate(self): self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
dunning = create_dunning_with_zero_interest_rate() self.assertEqual(round(dunning.total_interest, 2), 0.00)
amounts = calculate_interest_and_amount( self.assertEqual(round(dunning.dunning_fee, 2), 0.00)
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days self.assertEqual(round(dunning.dunning_amount, 2), 0.00)
) self.assertEqual(round(dunning.grand_total, 2), 100.00)
self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
def test_gl_entries(self): def test_dunning_with_fees_and_interest(self):
dunning = create_dunning() dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit()
gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
order by account asc""",
dunning.name,
as_dict=1,
)
self.assertTrue(gl_entries)
expected_values = dict(
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
def test_payment_entry(self): self.assertEqual(round(dunning.total_outstanding, 2), 100.00)
dunning = create_dunning() self.assertEqual(round(dunning.total_interest, 2), 0.41)
self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
self.assertEqual(round(dunning.grand_total, 2), 110.41)
def test_dunning_with_payment_entry(self):
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit() dunning.submit()
pe = get_payment_entry("Dunning", dunning.name) pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = nowdate() pe.reference_date = nowdate()
pe.paid_from_account_currency = dunning.currency
pe.paid_to_account_currency = dunning.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.insert() pe.insert()
pe.submit() pe.submit()
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0) for overdue_payment in dunning.overdue_payments:
outstanding_amount = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
self.assertEqual(outstanding_amount, 0)
dunning.reload()
self.assertEqual(dunning.status, "Resolved")
def test_dunning_and_payment_against_partially_due_invoice(self):
"""
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
"""
create_payment_terms_template_for_dunning()
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -1 * 6),
qty=1,
rate=100,
do_not_submit=True,
)
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning"
sales_invoice.submit()
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
self.assertEqual(len(dunning.overdue_payments), 1)
self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning")
dunning.submit()
pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no, pe.reference_date = "2", nowdate()
pe.insert()
pe.submit()
sales_invoice.load_from_db()
dunning.load_from_db()
self.assertEqual(sales_invoice.status, "Partly Paid")
self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
self.assertEqual(dunning.status, "Resolved")
# Test impact on cancellation of PE
pe.cancel()
sales_invoice.reload()
dunning.reload()
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(): def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -20) posting_date = add_days(today(), -1 * overdue_days)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center( sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status="Overdue" posting_date=posting_date, qty=1, rate=100
) )
dunning_type = frappe.get_doc("Dunning Type", "First Notice") dunning = create_dunning_from_sales_invoice(sales_invoice.name)
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name if dunning_type_name:
dunning.customer_name = sales_invoice.customer_name dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
dunning.outstanding_amount = sales_invoice.outstanding_amount dunning.dunning_type = dunning_type.name
dunning.debit_to = sales_invoice.debit_to dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.currency = sales_invoice.currency dunning.dunning_fee = dunning_type.dunning_fee
dunning.company = sales_invoice.company dunning.income_account = dunning_type.income_account
dunning.posting_date = nowdate() dunning.cost_center = dunning_type.cost_center
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice" return dunning.save()
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_with_zero_interest_rate(): def create_dunning_type(title, fee, interest, is_default):
posting_date = add_days(today(), -20) company = "_Test Company"
due_date = add_days(today(), -15) if frappe.db.exists("Dunning Type", f"{title} - _TC"):
sales_invoice = create_sales_invoice_against_cost_center( return
posting_date=posting_date, due_date=due_date, status="Overdue"
)
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
dunning.outstanding_amount = sales_invoice.outstanding_amount
dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = "First Notice" dunning_type.dunning_type = title
dunning_type.start_day = 10 dunning_type.company = company
dunning_type.end_day = 20 dunning_type.is_default = is_default
dunning_type.dunning_fee = 20 dunning_type.dunning_fee = fee
dunning_type.rate_of_interest = 8 dunning_type.rate_of_interest = interest
dunning_type.income_account = get_income_account(company)
dunning_type.cost_center = get_default_cost_center(company)
dunning_type.append( dunning_type.append(
"dunning_letter_text", "dunning_letter_text",
{ {
"language": "en", "language": "en",
"body_text": "We have still not received payment for our invoice ", "body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
}, },
) )
dunning_type.save() dunning_type.insert()
def create_dunning_type_with_zero_interest_rate(): def get_income_account(company):
dunning_type = frappe.new_doc("Dunning Type") return (
dunning_type.dunning_type = "First Notice with 0% Rate of Interest" frappe.get_value("Company", company, "default_income_account")
dunning_type.start_day = 10 or frappe.get_all(
dunning_type.end_day = 20 "Account",
dunning_type.dunning_fee = 20 filters={"is_group": 0, "company": company},
dunning_type.rate_of_interest = 0 or_filters={
dunning_type.append( "report_type": "Profit and Loss",
"dunning_letter_text", "account_type": ("in", ("Income Account", "Temporary")),
{ },
"language": "en", limit=1,
"body_text": "We have still not received payment for our invoice ", pluck="name",
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.", )[0]
},
) )
dunning_type.save()
def create_payment_terms_template_for_dunning():
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
create_payment_term("_Test Payment Term 1 for Dunning")
create_payment_term("_Test Payment Term 2 for Dunning")
if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50 for Dunning",
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 1 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 5,
},
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 2 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 10,
},
],
}
).insert()

View File

@ -1,8 +1,24 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on('Dunning Type', { frappe.ui.form.on("Dunning Type", {
// refresh: function(frm) { setup: function (frm) {
frm.set_query("income_account", () => {
// } return {
filters: {
root_type: "Income",
is_group: 0,
company: frm.doc.company,
},
};
});
frm.set_query("cost_center", () => {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
},
}); });

View File

@ -1,23 +1,26 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:dunning_type", "beta": 1,
"creation": "2019-12-04 04:59:08.003664", "creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"dunning_type", "dunning_type",
"overdue_interval_section", "is_default",
"start_day", "column_break_3",
"column_break_4", "company",
"end_day",
"section_break_6", "section_break_6",
"dunning_fee", "dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"text_block_section", "text_block_section",
"dunning_letter_text" "dunning_letter_text",
"section_break_9",
"income_account",
"column_break_13",
"cost_center"
], ],
"fields": [ "fields": [
{ {
@ -45,10 +48,6 @@
"fieldtype": "Table", "fieldtype": "Table",
"options": "Dunning Letter Text" "options": "Dunning Letter Text"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@ -57,33 +56,62 @@
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "overdue_interval_section",
"fieldtype": "Section Break",
"label": "Overdue Interval"
},
{
"fieldname": "start_day",
"fieldtype": "Int",
"label": "Start Day"
},
{
"fieldname": "end_day",
"fieldtype": "Int",
"label": "End Day"
},
{ {
"fieldname": "rate_of_interest", "fieldname": "rate_of_interest",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
},
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Income Account",
"options": "Account"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
} }
], ],
"links": [], "links": [
"modified": "2020-07-15 17:14:17.835074", {
"link_doctype": "Dunning",
"link_fieldname": "dunning_type"
}
],
"modified": "2021-11-13 00:25:35.659283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Type", "name": "Dunning Type",
"naming_rule": "By script",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@ -2,9 +2,11 @@
# For license information, please see license.txt # For license information, please see license.txt
# import frappe import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DunningType(Document): class DunningType(Document):
pass def autoname(self):
company_abbr = frappe.get_value("Company", self.company, "abbr")
self.name = f"{self.dunning_type} - {company_abbr}"

View File

@ -0,0 +1,36 @@
[
{
"doctype": "Dunning Type",
"dunning_type": "_Test First Notice",
"company": "_Test Company",
"is_default": 1,
"dunning_fee": 0.0,
"rate_of_interest": 0.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
},
{
"doctype": "Dunning Type",
"dunning_type": "_Test Second Notice",
"company": "_Test Company",
"is_default": 0,
"dunning_fee": 10.0,
"rate_of_interest": 10.0,
"dunning_letter_text": [
{
"language": "en",
"body_text": "We have still not received payment for our invoice",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees."
}
],
"income_account": "Sales - _TC",
"cost_center": "_Test Cost Center - _TC"
}
]

View File

@ -258,8 +258,8 @@ class ExchangeRateRevaluation(Document):
new_balance_in_base_currency = 0 new_balance_in_base_currency = 0
new_balance_in_account_currency = 0 new_balance_in_account_currency = 0
current_exchange_rate = calculate_exchange_rate_using_last_gle( current_exchange_rate = (
company, d.account, d.party_type, d.party calculate_exchange_rate_using_last_gle(company, d.account, d.party_type, d.party) or 0.0
) )
gain_loss = new_balance_in_account_currency - ( gain_loss = new_balance_in_account_currency - (

View File

@ -8,17 +8,6 @@ frappe.ui.form.on('Fiscal Year', {
frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1)); frappe.datetime.add_days(frappe.defaults.get_default("year_end_date"), 1));
} }
}, },
refresh: function (frm) {
if (!frm.doc.__islocal && (frm.doc.name != frappe.sys_defaults.fiscal_year)) {
frm.add_custom_button(__("Set as Default"), () => frm.events.set_as_default(frm));
frm.set_intro(__("To set this Fiscal Year as Default, click on 'Set as Default'"));
} else {
frm.set_intro("");
}
},
set_as_default: function(frm) {
return frm.call('set_as_default');
},
year_start_date: function(frm) { year_start_date: function(frm) {
if (!frm.doc.is_short_year) { if (!frm.doc.is_short_year) {
let year_end_date = let year_end_date =

View File

@ -4,28 +4,12 @@
import frappe import frappe
from dateutil.relativedelta import relativedelta from dateutil.relativedelta import relativedelta
from frappe import _, msgprint from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate from frappe.utils import add_days, add_years, cstr, getdate
class FiscalYear(Document): class FiscalYear(Document):
@frappe.whitelist()
def set_as_default(self):
frappe.db.set_single_value("Global Defaults", "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults")
global_defaults.check_permission("write")
global_defaults.on_update()
# clear cache
frappe.clear_cache()
msgprint(
_(
"{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
).format(self.name)
)
def validate(self): def validate(self):
self.validate_dates() self.validate_dates()
self.validate_overlap() self.validate_overlap()
@ -68,13 +52,6 @@ class FiscalYear(Document):
frappe.cache().delete_value("fiscal_years") frappe.cache().delete_value("fiscal_years")
def on_trash(self): def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name:
frappe.throw(
_(
"You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
).format(self.name)
)
frappe.cache().delete_value("fiscal_years") frappe.cache().delete_value("fiscal_years")
def validate_overlap(self): def validate_overlap(self):

View File

@ -35,6 +35,7 @@
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1, "in_list_view": 1,
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
@ -56,7 +57,7 @@
} }
], ],
"links": [], "links": [],
"modified": "2022-01-18 21:11:23.105589", "modified": "2023-07-09 18:11:23.105589",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Item Tax Template", "name": "Item Tax Template",

View File

@ -408,6 +408,15 @@ class JournalEntry(AccountsController):
d.idx, d.account d.idx, d.account
) )
) )
elif (
d.party_type
and frappe.db.get_value("Party Type", d.party_type, "account_type") != account_type
):
frappe.throw(
_("Row {0}: Account {1} and Party Type {2} have different account types").format(
d.idx, d.account, d.party_type
)
)
def check_credit_limit(self): def check_credit_limit(self):
customers = list( customers = list(

View File

@ -0,0 +1,170 @@
{
"actions": [],
"creation": "2021-09-15 18:34:27.172906",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"sales_invoice",
"payment_schedule",
"dunning_level",
"payment_term",
"section_break_15",
"description",
"section_break_4",
"due_date",
"overdue_days",
"mode_of_payment",
"column_break_5",
"invoice_portion",
"section_break_16",
"payment_amount",
"outstanding",
"paid_amount",
"discounted_amount",
"interest"
],
"fields": [
{
"columns": 2,
"fieldname": "payment_term",
"fieldtype": "Link",
"label": "Payment Term",
"options": "Payment Term",
"print_hide": 1,
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "section_break_15",
"fieldtype": "Section Break",
"label": "Description"
},
{
"columns": 2,
"fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"columns": 2,
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
"options": "Mode of Payment",
"read_only": 1
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"columns": 2,
"fieldname": "invoice_portion",
"fieldtype": "Percent",
"label": "Invoice Portion",
"read_only": 1
},
{
"columns": 2,
"fieldname": "payment_amount",
"fieldtype": "Currency",
"label": "Payment Amount",
"options": "currency",
"read_only": 1
},
{
"fetch_from": "payment_amount",
"fieldname": "outstanding",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Outstanding",
"options": "currency",
"read_only": 1
},
{
"depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount",
"options": "currency"
},
{
"default": "0",
"depends_on": "discounted_amount",
"fieldname": "discounted_amount",
"fieldtype": "Currency",
"label": "Discounted Amount",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"read_only": 1,
"reqd": 1
},
{
"fieldname": "payment_schedule",
"fieldtype": "Data",
"label": "Payment Schedule",
"print_hide": 1,
"read_only": 1
},
{
"fieldname": "overdue_days",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Overdue Days",
"read_only": 1
},
{
"default": "1",
"fieldname": "dunning_level",
"fieldtype": "Int",
"in_list_view": 1,
"label": "Dunning Level",
"read_only": 1
},
{
"fieldname": "section_break_16",
"fieldtype": "Section Break"
},
{
"fieldname": "interest",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Interest",
"options": "currency",
"read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-23 13:48:27.898830",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Overdue Payment",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class OverduePayment(Document):
pass

View File

@ -226,10 +226,12 @@ class PaymentEntry(AccountsController):
latest_lookup = {} latest_lookup = {}
for d in latest_references: for d in latest_references:
d = frappe._dict(d) d = frappe._dict(d)
latest_lookup.update({(d.voucher_type, d.voucher_no): d}) latest_lookup.setdefault((d.voucher_type, d.voucher_no), frappe._dict())[d.payment_term] = d
for d in self.get("references"): for d in self.get("references"):
latest = latest_lookup.get((d.reference_doctype, d.reference_name)) latest = (latest_lookup.get((d.reference_doctype, d.reference_name)) or frappe._dict()).get(
d.payment_term
)
# The reference has already been fully paid # The reference has already been fully paid
if not latest: if not latest:
@ -237,10 +239,9 @@ class PaymentEntry(AccountsController):
_("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name) _("{0} {1} has already been fully paid.").format(_(d.reference_doctype), d.reference_name)
) )
# The reference has already been partly paid # The reference has already been partly paid
elif ( elif latest.outstanding_amount < latest.invoice_amount and flt(
latest.outstanding_amount < latest.invoice_amount d.outstanding_amount, d.precision("outstanding_amount")
and flt(d.outstanding_amount, d.precision("outstanding_amount")) != latest.outstanding_amount ) != flt(latest.outstanding_amount, d.precision("outstanding_amount")):
):
frappe.throw( frappe.throw(
_( _(
"{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts." "{0} {1} has already been partly paid. Please use the 'Get Outstanding Invoice' or the 'Get Outstanding Orders' button to get the latest outstanding amounts."
@ -252,6 +253,18 @@ class PaymentEntry(AccountsController):
if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount): if (flt(d.allocated_amount)) > 0 and flt(d.allocated_amount) > flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx)) frappe.throw(fail_message.format(d.idx))
if d.payment_term and (
(flt(d.allocated_amount)) > 0
and flt(d.allocated_amount) > flt(latest.payment_term_outstanding)
):
frappe.throw(
_(
"Row #{0}: Allocated amount:{1} is greater than outstanding amount:{2} for Payment Term {3}"
).format(
d.idx, d.allocated_amount, latest.payment_term_outstanding, d.payment_term
)
)
# Check for negative outstanding invoices as well # Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount): if flt(d.allocated_amount) < 0 and flt(d.allocated_amount) < flt(latest.outstanding_amount):
frappe.throw(fail_message.format(d.idx)) frappe.throw(fail_message.format(d.idx))
@ -1433,6 +1446,9 @@ def get_outstanding_reference_documents(args, validate=False):
if args.get("party_type") == "Member": if args.get("party_type") == "Member":
return return
if not args.get("get_outstanding_invoices") and not args.get("get_orders_to_be_billed"):
args["get_outstanding_invoices"] = True
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
common_filter = [] common_filter = []
accounting_dimensions_filter = [] accounting_dimensions_filter = []
@ -1498,7 +1514,9 @@ def get_outstanding_reference_documents(args, validate=False):
accounting_dimensions=accounting_dimensions_filter, accounting_dimensions=accounting_dimensions_filter,
) )
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices) outstanding_invoices = split_invoices_based_on_payment_terms(
outstanding_invoices, args.get("company")
)
for d in outstanding_invoices: for d in outstanding_invoices:
d["exchange_rate"] = 1 d["exchange_rate"] = 1
@ -1558,8 +1576,27 @@ def get_outstanding_reference_documents(args, validate=False):
return data return data
def split_invoices_based_on_payment_terms(outstanding_invoices): def split_invoices_based_on_payment_terms(outstanding_invoices, company):
invoice_ref_based_on_payment_terms = {} invoice_ref_based_on_payment_terms = {}
company_currency = (
frappe.db.get_value("Company", company, "default_currency") if company else None
)
exc_rates = frappe._dict()
for doctype in ["Sales Invoice", "Purchase Invoice"]:
invoices = [x.voucher_no for x in outstanding_invoices if x.voucher_type == doctype]
for x in frappe.db.get_all(
doctype,
filters={"name": ["in", invoices]},
fields=["name", "currency", "conversion_rate", "party_account_currency"],
):
exc_rates[x.name] = frappe._dict(
conversion_rate=x.conversion_rate,
currency=x.currency,
party_account_currency=x.party_account_currency,
company_currency=company_currency,
)
for idx, d in enumerate(outstanding_invoices): for idx, d in enumerate(outstanding_invoices):
if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]: if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
payment_term_template = frappe.db.get_value( payment_term_template = frappe.db.get_value(
@ -1576,6 +1613,14 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
for payment_term in payment_schedule: for payment_term in payment_schedule:
if payment_term.outstanding > 0.1: if payment_term.outstanding > 0.1:
doc_details = exc_rates.get(payment_term.parent, None)
is_multi_currency_acc = (doc_details.currency != doc_details.company_currency) and (
doc_details.party_account_currency != doc_details.company_currency
)
payment_term_outstanding = flt(payment_term.outstanding)
if not is_multi_currency_acc:
payment_term_outstanding = doc_details.conversion_rate * flt(payment_term.outstanding)
invoice_ref_based_on_payment_terms.setdefault(idx, []) invoice_ref_based_on_payment_terms.setdefault(idx, [])
invoice_ref_based_on_payment_terms[idx].append( invoice_ref_based_on_payment_terms[idx].append(
frappe._dict( frappe._dict(
@ -1587,6 +1632,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
"posting_date": d.posting_date, "posting_date": d.posting_date,
"invoice_amount": flt(d.invoice_amount), "invoice_amount": flt(d.invoice_amount),
"outstanding_amount": flt(d.outstanding_amount), "outstanding_amount": flt(d.outstanding_amount),
"payment_term_outstanding": payment_term_outstanding,
"payment_amount": payment_term.payment_amount, "payment_amount": payment_term.payment_amount,
"payment_term": payment_term.payment_term, "payment_term": payment_term.payment_term,
"account": d.account, "account": d.account,
@ -1627,60 +1673,59 @@ def get_orders_to_be_billed(
cost_center=None, cost_center=None,
filters=None, filters=None,
): ):
voucher_type = None
if party_type == "Customer": if party_type == "Customer":
voucher_type = "Sales Order" voucher_type = "Sales Order"
elif party_type == "Supplier": elif party_type == "Supplier":
voucher_type = "Purchase Order" voucher_type = "Purchase Order"
elif party_type == "Employee":
voucher_type = None if not voucher_type:
return []
# Add cost center condition # Add cost center condition
if voucher_type: doc = frappe.get_doc({"doctype": voucher_type})
doc = frappe.get_doc({"doctype": voucher_type}) condition = ""
condition = "" if doc and hasattr(doc, "cost_center") and doc.cost_center:
if doc and hasattr(doc, "cost_center") and doc.cost_center: condition = " and cost_center='%s'" % cost_center
condition = " and cost_center='%s'" % cost_center
orders = [] if party_account_currency == company_currency:
if voucher_type: grand_total_field = "base_grand_total"
if party_account_currency == company_currency: rounded_total_field = "base_rounded_total"
grand_total_field = "base_grand_total" else:
rounded_total_field = "base_rounded_total" grand_total_field = "grand_total"
else: rounded_total_field = "rounded_total"
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
orders = frappe.db.sql( orders = frappe.db.sql(
""" """
select select
name as voucher_no, name as voucher_no,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount, if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
(if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount, (if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) - advance_paid) as outstanding_amount,
transaction_date as posting_date transaction_date as posting_date
from from
`tab{voucher_type}` `tab{voucher_type}`
where where
{party_type} = %s {party_type} = %s
and docstatus = 1 and docstatus = 1
and company = %s and company = %s
and ifnull(status, "") != "Closed" and ifnull(status, "") != "Closed"
and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid and if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) > advance_paid
and abs(100 - per_billed) > 0.01 and abs(100 - per_billed) > 0.01
{condition} {condition}
order by order by
transaction_date, name transaction_date, name
""".format( """.format(
**{ **{
"rounded_total_field": rounded_total_field, "rounded_total_field": rounded_total_field,
"grand_total_field": grand_total_field, "grand_total_field": grand_total_field,
"voucher_type": voucher_type, "voucher_type": voucher_type,
"party_type": scrub(party_type), "party_type": scrub(party_type),
"condition": condition, "condition": condition,
} }
), ),
(party, company), (party, company),
as_dict=True, as_dict=True,
) )
order_list = [] order_list = []
for d in orders: for d in orders:
@ -1713,6 +1758,8 @@ def get_negative_outstanding_invoices(
cost_center=None, cost_center=None,
condition=None, condition=None,
): ):
if party_type not in ["Customer", "Supplier"]:
return []
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
account = "debit_to" if voucher_type == "Sales Invoice" else "credit_to" account = "debit_to" if voucher_type == "Sales Invoice" else "credit_to"
supplier_condition = "" supplier_condition = ""
@ -2007,28 +2054,27 @@ def get_payment_entry(
pe.append("references", reference) pe.append("references", reference)
else: else:
if dt == "Dunning": if dt == "Dunning":
for overdue_payment in doc.overdue_payments:
pe.append(
"references",
{
"reference_doctype": "Sales Invoice",
"reference_name": overdue_payment.sales_invoice,
"payment_term": overdue_payment.payment_term,
"due_date": overdue_payment.due_date,
"total_amount": overdue_payment.outstanding,
"outstanding_amount": overdue_payment.outstanding,
"allocated_amount": overdue_payment.outstanding,
},
)
pe.append( pe.append(
"references", "deductions",
{ {
"reference_doctype": "Sales Invoice", "account": doc.income_account,
"reference_name": doc.get("sales_invoice"), "cost_center": doc.cost_center,
"bill_no": doc.get("bill_no"), "amount": -1 * doc.dunning_amount,
"due_date": doc.get("due_date"), "description": _("Interest and/or dunning fee"),
"total_amount": doc.get("outstanding_amount"),
"outstanding_amount": doc.get("outstanding_amount"),
"allocated_amount": doc.get("outstanding_amount"),
},
)
pe.append(
"references",
{
"reference_doctype": dt,
"reference_name": dn,
"bill_no": doc.get("bill_no"),
"due_date": doc.get("due_date"),
"total_amount": doc.get("dunning_amount"),
"outstanding_amount": doc.get("dunning_amount"),
"allocated_amount": doc.get("dunning_amount"),
}, },
) )
else: else:
@ -2122,8 +2168,10 @@ def set_party_account_currency(dt, party_account, doc):
def set_payment_type(dt, doc): def set_payment_type(dt, doc):
if ( if (
dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0) (dt == "Sales Order" or (dt == "Sales Invoice" and doc.outstanding_amount > 0))
) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0): or (dt == "Purchase Invoice" and doc.outstanding_amount < 0)
or dt == "Dunning"
):
payment_type = "Receive" payment_type = "Receive"
else: else:
payment_type = "Pay" payment_type = "Pay"
@ -2368,6 +2416,7 @@ def get_reference_as_per_payment_terms(
"due_date": doc.get("due_date"), "due_date": doc.get("due_date"),
"total_amount": grand_total, "total_amount": grand_total,
"outstanding_amount": outstanding_amount, "outstanding_amount": outstanding_amount,
"payment_term_outstanding": payment_term_outstanding,
"payment_term": payment_term.payment_term, "payment_term": payment_term.payment_term,
"allocated_amount": payment_term_outstanding, "allocated_amount": payment_term_outstanding,
} }

View File

@ -1061,6 +1061,101 @@ class TestPaymentEntry(FrappeTestCase):
} }
self.assertDictEqual(ref_details, expected_response) self.assertDictEqual(ref_details, expected_response)
@change_settings(
"Accounts Settings",
{
"unlink_payment_on_cancellation_of_invoice": 1,
"delete_linked_ledger_entries": 1,
"allow_multi_currency_invoices_against_single_party_account": 1,
},
)
def test_overallocation_validation_on_payment_terms(self):
"""
Validate Allocation on Payment Entry based on Payment Schedule. Upon overallocation, validation error must be thrown.
"""
customer = create_customer()
create_payment_terms_template()
# Validate allocation on base/company currency
si1 = create_sales_invoice(do_not_save=1, qty=1, rate=200)
si1.payment_terms_template = "Test Receivable Template"
si1.save().submit()
si1.reload()
pe = get_payment_entry(si1.doctype, si1.name).save()
# Allocated amount should be according to the payment schedule
for idx, schedule in enumerate(si1.payment_schedule):
with self.subTest(idx=idx):
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 400
pe.references[0].allocated_amount = 200
pe.references[1].allocated_amount = 200
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si1.cancel()
si1.delete()
# Validate allocation on foreign currency
si2 = create_sales_invoice(
customer="_Test Customer USD",
debit_to="_Test Receivable USD - _TC",
currency="USD",
conversion_rate=80,
do_not_save=1,
)
si2.payment_terms_template = "Test Receivable Template"
si2.save().submit()
si2.reload()
pe = get_payment_entry(si2.doctype, si2.name).save()
# Allocated amount should be according to the payment schedule
for idx, schedule in enumerate(si2.payment_schedule):
with self.subTest(idx=idx):
self.assertEqual(flt(schedule.payment_amount), flt(pe.references[idx].allocated_amount))
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 200
pe.references[0].allocated_amount = 100
pe.references[1].allocated_amount = 100
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si2.cancel()
si2.delete()
# Validate allocation in base/company currency on a foreign currency document
# when invoice is made is foreign currency, but posted to base/company currency debtors account
si3 = create_sales_invoice(
customer=customer,
currency="USD",
conversion_rate=80,
do_not_save=1,
)
si3.payment_terms_template = "Test Receivable Template"
si3.save().submit()
si3.reload()
pe = get_payment_entry(si3.doctype, si3.name).save()
# Allocated amount should be equal to payment term outstanding
self.assertEqual(len(pe.references), 2)
for idx, ref in enumerate(pe.references):
with self.subTest(idx=idx):
self.assertEqual(ref.payment_term_outstanding, ref.allocated_amount)
pe.save()
# Overallocation validation should trigger
pe.paid_amount = 16000
pe.references[0].allocated_amount = 8000
pe.references[1].allocated_amount = 8000
self.assertRaises(frappe.ValidationError, pe.save)
pe.delete()
si3.cancel()
si3.delete()
def create_payment_entry(**args): def create_payment_entry(**args):
payment_entry = frappe.new_doc("Payment Entry") payment_entry = frappe.new_doc("Payment Entry")
@ -1150,3 +1245,17 @@ def create_payment_terms_template_with_discount(
def create_payment_term(name): def create_payment_term(name):
if not frappe.db.exists("Payment Term", name): if not frappe.db.exists("Payment Term", name):
frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert() frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
def create_customer(name="_Test Customer 2 USD", currency="USD"):
customer = None
if frappe.db.exists("Customer", name):
customer = name
else:
customer = frappe.new_doc("Customer")
customer.customer_name = name
customer.default_currency = currency
customer.type = "Individual"
customer.save()
customer = customer.name
return customer

View File

@ -126,21 +126,22 @@ class PeriodClosingVoucher(AccountsController):
def make_gl_entries(self, get_opening_entries=False): def make_gl_entries(self, get_opening_entries=False):
gl_entries = self.get_gl_entries() gl_entries = self.get_gl_entries()
closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries) closing_entries = self.get_grouped_gl_entries(get_opening_entries=get_opening_entries)
if gl_entries: if len(gl_entries) > 5000:
if len(gl_entries) > 5000: frappe.enqueue(
frappe.enqueue( process_gl_entries,
process_gl_entries, gl_entries=gl_entries,
gl_entries=gl_entries, closing_entries=closing_entries,
closing_entries=closing_entries, voucher_name=self.name,
voucher_name=self.name, company=self.company,
queue="long", closing_date=self.posting_date,
) queue="long",
frappe.msgprint( )
_("The GL Entries will be processed in the background, it can take a few minutes."), frappe.msgprint(
alert=True, _("The GL Entries will be processed in the background, it can take a few minutes."),
) alert=True,
else: )
process_gl_entries(gl_entries, closing_entries, voucher_name=self.name) else:
process_gl_entries(gl_entries, closing_entries, self.name, self.company, self.posting_date)
def get_grouped_gl_entries(self, get_opening_entries=False): def get_grouped_gl_entries(self, get_opening_entries=False):
closing_entries = [] closing_entries = []
@ -321,24 +322,22 @@ class PeriodClosingVoucher(AccountsController):
return query.run(as_dict=1) return query.run(as_dict=1)
def process_gl_entries(gl_entries, closing_entries, voucher_name=None): def process_gl_entries(gl_entries, closing_entries, voucher_name, company, closing_date):
from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import ( from erpnext.accounts.doctype.account_closing_balance.account_closing_balance import (
make_closing_entries, make_closing_entries,
) )
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
try: try:
make_gl_entries(gl_entries, merge_entries=False) if gl_entries:
make_closing_entries(gl_entries + closing_entries, voucher_name=voucher_name) make_gl_entries(gl_entries, merge_entries=False)
frappe.db.set_value(
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" make_closing_entries(gl_entries + closing_entries, voucher_name, company, closing_date)
) frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Completed")
except Exception as e: except Exception as e:
frappe.db.rollback() frappe.db.rollback()
frappe.log_error(e) frappe.log_error(e)
frappe.db.set_value( frappe.db.set_value("Period Closing Voucher", voucher_name, "gle_processing_status", "Failed")
"Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed"
)
def make_reverse_gl_entries(voucher_type, voucher_no): def make_reverse_gl_entries(voucher_type, voucher_no):

View File

@ -142,9 +142,15 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
cur_frm.events.create_invoice_discounting(cur_frm); cur_frm.events.create_invoice_discounting(cur_frm);
}, __('Create')); }, __('Create'));
if (doc.due_date < frappe.datetime.get_today()) { const payment_is_overdue = doc.payment_schedule.map(
cur_frm.add_custom_button(__('Dunning'), function() { row => Date.parse(row.due_date) < Date.now()
cur_frm.events.create_dunning(cur_frm); ).reduce(
(prev, current) => prev || current
);
if (payment_is_overdue) {
this.frm.add_custom_button(__('Dunning'), () => {
this.frm.events.create_dunning(this.frm);
}, __('Create')); }, __('Create'));
} }
} }

View File

@ -2516,55 +2516,49 @@ def get_mode_of_payment_info(mode_of_payment, company):
@frappe.whitelist() @frappe.whitelist()
def create_dunning(source_name, target_doc=None): def create_dunning(source_name, target_doc=None, ignore_permissions=False):
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import ( def postprocess_dunning(source, target):
calculate_interest_and_amount, from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text
get_dunning_letter_text,
)
def set_missing_values(source, target): dunning_type = frappe.db.exists("Dunning Type", {"is_default": 1, "company": source.company})
target.sales_invoice = source_name if dunning_type:
target.outstanding_amount = source.outstanding_amount dunning_type = frappe.get_doc("Dunning Type", dunning_type)
overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days
target.overdue_days = overdue_days
if frappe.db.exists(
"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
):
dunning_type = frappe.get_doc(
"Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]}
)
target.dunning_type = dunning_type.name target.dunning_type = dunning_type.name
target.rate_of_interest = dunning_type.rate_of_interest target.rate_of_interest = dunning_type.rate_of_interest
target.dunning_fee = dunning_type.dunning_fee target.dunning_fee = dunning_type.dunning_fee
letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) target.income_account = dunning_type.income_account
target.cost_center = dunning_type.cost_center
letter_text = get_dunning_letter_text(
dunning_type=dunning_type.name, doc=target.as_dict(), language=source.language
)
if letter_text: if letter_text:
target.body_text = letter_text.get("body_text") target.body_text = letter_text.get("body_text")
target.closing_text = letter_text.get("closing_text") target.closing_text = letter_text.get("closing_text")
target.language = letter_text.get("language") target.language = letter_text.get("language")
amounts = calculate_interest_and_amount(
target.outstanding_amount,
target.rate_of_interest,
target.dunning_fee,
target.overdue_days,
)
target.interest_amount = amounts.get("interest_amount")
target.dunning_amount = amounts.get("dunning_amount")
target.grand_total = amounts.get("grand_total")
doclist = get_mapped_doc( target.validate()
"Sales Invoice",
source_name, return get_mapped_doc(
{ from_doctype="Sales Invoice",
from_docname=source_name,
target_doc=target_doc,
table_maps={
"Sales Invoice": { "Sales Invoice": {
"doctype": "Dunning", "doctype": "Dunning",
} "field_map": {"customer_address": "customer_address", "parent": "sales_invoice"},
},
"Payment Schedule": {
"doctype": "Overdue Payment",
"field_map": {"name": "payment_schedule", "parent": "sales_invoice"},
"condition": lambda doc: doc.outstanding > 0 and getdate(doc.due_date) < getdate(),
},
}, },
target_doc, postprocess=postprocess_dunning,
set_missing_values, ignore_permissions=ignore_permissions,
) )
return doclist
def check_if_return_invoice_linked_with_payment_entry(self): def check_if_return_invoice_linked_with_payment_entry(self):

View File

@ -3,8 +3,6 @@
frappe.ui.form.on('Shareholder', { frappe.ui.form.on('Shareholder', {
refresh: function(frm) { refresh: function(frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Shareholder' };
frm.toggle_display(['contact_html'], !frm.doc.__islocal); frm.toggle_display(['contact_html'], !frm.doc.__islocal);
if (frm.doc.__islocal) { if (frm.doc.__islocal) {

View File

@ -13,14 +13,11 @@ import erpnext
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions, get_accounting_dimensions,
) )
from erpnext.accounts.doctype.accounting_period.accounting_period import ClosedAccountingPeriod
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.utils import create_payment_ledger_entry from erpnext.accounts.utils import create_payment_ledger_entry
class ClosedAccountingPeriod(frappe.ValidationError):
pass
def make_gl_entries( def make_gl_entries(
gl_map, gl_map,
cancel=False, cancel=False,

View File

@ -1,4 +1,5 @@
{ {
"absolute_value": 0,
"align_labels_right": 0, "align_labels_right": 0,
"creation": "2019-12-11 04:37:14.012805", "creation": "2019-12-11 04:37:14.012805",
"css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n", "css": ".print-format th {\n background-color: transparent !important;\n border-bottom: 1px solid !important;\n border-top: none !important;\n}\n.print-format .ql-editor {\n padding-left: 0px;\n padding-right: 0px;\n}\n\n.print-format table {\n margin-bottom: 0px;\n }\n.print-format .table-data tr:last-child { \n border-bottom: 1px solid !important;\n}\n\n.print-format .table-inner tr:last-child {\n border-bottom:none !important;\n}\n.print-format .table-inner {\n margin: 0px 0px;\n}\n\n.print-format .table-data ul li { \n color:#787878 !important;\n}\n\n.no-top-border {\n border-top:none !important;\n}\n\n.table-inner td {\n padding-left: 0px !important; \n padding-top: 1px !important;\n padding-bottom: 1px !important;\n color:#787878 !important;\n}\n\n.total {\n background-color: lightgrey !important;\n padding-top: 4px !important;\n padding-bottom: 4px !important;\n}\n",
@ -9,10 +10,10 @@
"docstatus": 0, "docstatus": 0,
"doctype": "Print Format", "doctype": "Print Format",
"font": "Arial", "font": "Arial",
"format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldname\": \"sales_invoice\", \"print_hide\": 0, \"label\": \"Sales Invoice\"}, {\"fieldname\": \"due_date\", \"print_hide\": 0, \"label\": \"Due Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<table class=\\\"table table-borderless table-data\\\">\\n <tbody>\\n <tr>\\n <th>{{_(\\\"Description\\\")}}</th>\\n\\t <th style=\\\"text-align: right;\\\">{{_(\\\"Amount\\\")}}</th>\\n </tr>\\n <tr>\\n <td>\\n {{_(\\\"Outstanding Amount\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"outstanding_amount\\\")}}\\n </td>\\n </tr>\\n {%if doc.rate_of_interest > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Interest \\\")}} {{doc.rate_of_interest}}% p.a. ({{doc.overdue_days}} {{_(\\\"days\\\")}})\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"interest_amount\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n {%if doc.dunning_fee > 0%}\\n <tr>\\n <td>\\n {{_(\\\"Dunning Fee\\\")}}\\n </td>\\n <td style=\\\"text-align: right;\\\">\\n {{doc.get_formatted(\\\"dunning_fee\\\")}}\\n </td>\\n </tr>\\n {% endif %}\\n </tbody>\\n</table>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"\\n<div class=\\\"row total\\\" style =\\\"margin-right: 0px;\\\">\\n\\t\\t<div class=\\\"col-xs-5\\\">\\n\\t\\t\\t<b>{{_(\\\"Grand Total\\\")}}</b></div>\\n\\t\\t<div class=\\\"col-xs-7 text-right\\\" style=\\\"padding-right: 4px;\\\">\\n\\t\\t\\t<b>{{doc.get_formatted(\\\"grand_total\\\")}}</b>\\n\\t\\t</div>\\n</div>\\n\\n\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]", "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<b>{{doc.customer_name}}</b> <br />\\n{{doc.address_display}}\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"_custom_html\", \"print_hide\": 0, \"label\": \"Custom HTML\", \"fieldtype\": \"HTML\", \"options\": \"<div style=\\\"text-align:left;\\\">\\n<div style=\\\"font-size:24px; text-transform:uppercase;\\\">{{_(doc.dunning_type)}}</div>\\n<div style=\\\"font-size:16px;padding-bottom:5px;\\\">{{ doc.name }}</div>\\n</div>\"}, {\"fieldname\": \"posting_date\", \"print_hide\": 0, \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"body_text\", \"print_hide\": 0, \"label\": \"Body Text\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"overdue_payments\", \"print_hide\": 0, \"label\": \"Overdue Payments\", \"visible_columns\": [{\"fieldname\": \"sales_invoice\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"dunning_level\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"due_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"overdue_days\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"invoice_portion\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"outstanding\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"interest\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"total_outstanding\", \"print_hide\": 0, \"label\": \"Total Outstanding\"}, {\"fieldname\": \"dunning_fee\", \"print_hide\": 0, \"label\": \"Dunning Fee\"}, {\"fieldname\": \"total_interest\", \"print_hide\": 0, \"label\": \"Total Interest\"}, {\"fieldname\": \"grand_total\", \"print_hide\": 0, \"label\": \"Grand Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"closing_text\", \"print_hide\": 0, \"label\": \"Closing Text\"}]",
"idx": 0, "idx": 0,
"line_breaks": 0, "line_breaks": 0,
"modified": "2020-07-14 18:25:44.348207", "modified": "2021-09-30 10:22:02.603871",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Letter", "name": "Dunning Letter",

View File

@ -49,7 +49,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Start Year"), "label": __("Start Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
on_change: () => { on_change: () => {
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('from_fiscal_year'), function(r) {
@ -65,7 +65,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("End Year"), "label": __("End Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
on_change: () => { on_change: () => {
frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) { frappe.model.with_doc("Fiscal Year", frappe.query_report.get_filter_value('to_fiscal_year'), function(r) {
@ -139,7 +139,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
return value; return value;
}, },
onload: function() { onload: function() {
let fiscal_year = frappe.defaults.get_user_default("fiscal_year") let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);

View File

@ -48,7 +48,7 @@ function get_filters() {
"label": __("Start Year"), "label": __("Start Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1 "reqd": 1
}, },
{ {
@ -56,7 +56,7 @@ function get_filters() {
"label": __("End Year"), "label": __("End Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1 "reqd": 1
}, },
{ {
@ -100,7 +100,7 @@ frappe.query_reports["Deferred Revenue and Expense"] = {
return default_formatter(value, row, column, data); return default_formatter(value, row, column, data);
}, },
onload: function(report){ onload: function(report){
let fiscal_year = frappe.defaults.get_user_default("fiscal_year"); let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);

View File

@ -4,9 +4,10 @@
import frappe import frappe
from frappe import _, qb from frappe import _, qb
from frappe.query_builder import Column, functions from frappe.query_builder import Column, functions
from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, getdate, rounded
from erpnext.accounts.report.financial_statements import get_period_list from erpnext.accounts.report.financial_statements import get_period_list
from erpnext.accounts.utils import get_fiscal_year
class Deferred_Item(object): class Deferred_Item(object):
@ -226,7 +227,7 @@ class Deferred_Revenue_and_Expense_Report(object):
# If no filters are provided, get user defaults # If no filters are provided, get user defaults
if not filters: if not filters:
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date=getdate()))
self.filters = frappe._dict( self.filters = frappe._dict(
{ {
"company": frappe.defaults.get_user_default("Company"), "company": frappe.defaults.get_user_default("Company"),

View File

@ -10,6 +10,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import ( from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import (
Deferred_Revenue_and_Expense_Report, Deferred_Revenue_and_Expense_Report,
) )
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.buying.doctype.supplier.test_supplier import create_supplier
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
@ -116,7 +117,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
pda.submit() pda.submit()
# execute report # execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict( self.filters = frappe._dict(
{ {
"company": frappe.defaults.get_user_default("Company"), "company": frappe.defaults.get_user_default("Company"),
@ -209,7 +210,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
pda.submit() pda.submit()
# execute report # execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict( self.filters = frappe._dict(
{ {
"company": frappe.defaults.get_user_default("Company"), "company": frappe.defaults.get_user_default("Company"),
@ -297,7 +298,7 @@ class TestDeferredRevenueAndExpense(unittest.TestCase):
pda.submit() pda.submit()
# execute report # execute report
fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) fiscal_year = frappe.get_doc("Fiscal Year", get_fiscal_year(date="2021-05-01"))
self.filters = frappe._dict( self.filters = frappe._dict(
{ {
"company": frappe.defaults.get_user_default("Company"), "company": frappe.defaults.get_user_default("Company"),

View File

@ -18,7 +18,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"on_change": function(query_report) { "on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year; var fiscal_year = query_report.get_values().fiscal_year;

View File

@ -416,6 +416,7 @@ def set_gl_entries_by_account(
filters, filters,
gl_entries_by_account, gl_entries_by_account,
ignore_closing_entries=False, ignore_closing_entries=False,
ignore_opening_entries=False,
): ):
"""Returns a dict like { "account": [gl entries], ... }""" """Returns a dict like { "account": [gl entries], ... }"""
gl_entries = [] gl_entries = []
@ -426,7 +427,6 @@ def set_gl_entries_by_account(
pluck="name", pluck="name",
) )
ignore_opening_entries = False
if accounts_list: if accounts_list:
# For balance sheet # For balance sheet
if not from_date: if not from_date:

View File

@ -12,14 +12,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
erpnext.financial_statements); erpnext.financial_statements);
frappe.query_reports["Gross and Net Profit Report"]["filters"].push( frappe.query_reports["Gross and Net Profit Report"]["filters"].push(
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt);
}
},
{ {
"fieldname": "accumulated_values", "fieldname": "accumulated_values",
"label": __("Accumulated Values"), "label": __("Accumulated Values"),

View File

@ -9,16 +9,6 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
erpnext.utils.add_dimensions('Profit and Loss Statement', 10); erpnext.utils.add_dimensions('Profit and Loss Statement', 10);
frappe.query_reports["Profit and Loss Statement"]["filters"].push( frappe.query_reports["Profit and Loss Statement"]["filters"].push(
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt, {
company: frappe.query_report.get_filter_value("company")
});
},
},
{ {
"fieldname": "include_default_book_entries", "fieldname": "include_default_book_entries",
"label": __("Include Default Book Entries"), "label": __("Include Default Book Entries"),

View File

@ -25,7 +25,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"on_change": function(query_report) { "on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year; var fiscal_year = query_report.get_values().fiscal_year;

View File

@ -17,7 +17,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"on_change": function(query_report) { "on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year; var fiscal_year = query_report.get_values().fiscal_year;

View File

@ -117,6 +117,7 @@ def get_data(filters):
filters, filters,
gl_entries_by_account, gl_entries_by_account,
ignore_closing_entries=not flt(filters.with_period_closing_entry), ignore_closing_entries=not flt(filters.with_period_closing_entry),
ignore_opening_entries=True,
) )
calculate_values(accounts, gl_entries_by_account, opening_balances) calculate_values(accounts, gl_entries_by_account, opening_balances)
@ -159,6 +160,8 @@ def get_rootwise_opening_balances(filters, report_type):
accounting_dimensions, accounting_dimensions,
period_closing_voucher=last_period_closing_voucher[0].name, period_closing_voucher=last_period_closing_voucher[0].name,
) )
# Report getting generate from the mid of a fiscal year
if getdate(last_period_closing_voucher[0].posting_date) < getdate( if getdate(last_period_closing_voucher[0].posting_date) < getdate(
add_days(filters.from_date, -1) add_days(filters.from_date, -1)
): ):
@ -218,9 +221,18 @@ def get_opening_balance(
) )
else: else:
if start_date: if start_date:
opening_balance = opening_balance.where(closing_balance.posting_date >= start_date) opening_balance = opening_balance.where(
(closing_balance.posting_date >= start_date)
& (closing_balance.posting_date < filters.from_date)
)
opening_balance = opening_balance.where(closing_balance.is_opening == "No") opening_balance = opening_balance.where(closing_balance.is_opening == "No")
opening_balance = opening_balance.where(closing_balance.posting_date < filters.from_date) else:
opening_balance = opening_balance.where(
(closing_balance.posting_date < filters.from_date) | (closing_balance.is_opening == "Yes")
)
if doctype == "GL Entry":
opening_balance = opening_balance.where(closing_balance.is_cancelled == 0)
if ( if (
not filters.show_unclosed_fy_pl_balances not filters.show_unclosed_fy_pl_balances

View File

@ -16,7 +16,7 @@ frappe.query_reports["Trial Balance for Party"] = {
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"on_change": function(query_report) { "on_change": function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year; var fiscal_year = query_report.get_values().fiscal_year;

View File

@ -850,7 +850,7 @@ def get_held_invoices(party_type, party):
if party_type == "Supplier": if party_type == "Supplier":
held_invoices = frappe.db.sql( held_invoices = frappe.db.sql(
"select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()", "select name from `tabPurchase Invoice` where on_hold = 1 and release_date IS NOT NULL and release_date > CURDATE()",
as_dict=1, as_dict=1,
) )
held_invoices = set(d["name"] for d in held_invoices) held_invoices = set(d["name"] for d in held_invoices)
@ -1110,6 +1110,12 @@ def get_autoname_with_number(number_value, doc_title, company):
return " - ".join(parts) return " - ".join(parts)
def parse_naming_series_variable(doc, variable):
if variable == "FY":
date = doc.get("posting_date") or doc.get("transaction_date") or getdate()
return get_fiscal_year(date=date, company=doc.get("company"))[0]
@frappe.whitelist() @frappe.whitelist()
def get_coa(doctype, parent, is_root, chart=None): def get_coa(doctype, parent, is_root, chart=None):
from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import ( from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import (

View File

@ -63,7 +63,7 @@ frappe.ui.form.on('Asset Movement', {
fieldnames_to_be_altered = { fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 }, target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 1, reqd: 0 }, source_location: { read_only: 1, reqd: 0 },
from_employee: { read_only: 0, reqd: 1 }, from_employee: { read_only: 0, reqd: 0 },
to_employee: { read_only: 1, reqd: 0 } to_employee: { read_only: 1, reqd: 0 }
}; };
} }

View File

@ -62,29 +62,20 @@ class AssetMovement(Document):
frappe.throw(_("Source and Target Location cannot be same")) frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt": if self.purpose == "Receipt":
# only when asset is bought and first entry is made if not (d.source_location or d.from_employee) and not (d.target_location or d.to_employee):
if not d.source_location and not (d.target_location or d.to_employee):
frappe.throw( frappe.throw(
_("Target Location or To Employee is required while receiving Asset {0}").format(d.asset) _("Target Location or To Employee is required while receiving Asset {0}").format(d.asset)
) )
elif d.source_location: elif d.from_employee and not d.target_location:
# when asset is received from an employee frappe.throw(
if d.target_location and not d.from_employee: _("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
frappe.throw( )
_("From employee is required while receiving Asset {0} to a target location").format( elif d.to_employee and d.target_location:
d.asset frappe.throw(
) _(
) "Asset {0} cannot be received at a location and given to an employee in a single movement"
if d.from_employee and not d.target_location: ).format(d.asset)
frappe.throw( )
_("Target Location is required while receiving Asset {0} from an employee").format(d.asset)
)
if d.to_employee and d.target_location:
frappe.throw(
_(
"Asset {0} cannot be received at a location and given to employee in a single movement"
).format(d.asset)
)
def validate_employee(self): def validate_employee(self):
for d in self.assets: for d in self.assets:

View File

@ -82,7 +82,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("Start Year"), "label": __("Start Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
}, },
{ {
@ -90,7 +90,7 @@ frappe.query_reports["Fixed Asset Register"] = {
"label": __("End Year"), "label": __("End Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"depends_on": "eval: doc.filter_based_on == 'Fiscal Year'", "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
}, },
{ {

View File

@ -5,18 +5,19 @@
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Item\"}",
"idx": 0, "idx": 1,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-21 16:13:25.092287", "modified": "2023-07-19 13:06:42.937941",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Trends", "name": "Purchase Order Trends",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Purchase Order Trends", "report_name": "Purchase Order Trends",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Line", "type": "Line",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -4,18 +4,19 @@
"creation": "2020-07-20 21:01:02.329519", "creation": "2020-07-20 21:01:02.329519",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}", "filters_json": "{\"period\":\"Monthly\",\"period_based_on\":\"posting_date\",\"based_on\":\"Supplier\"}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 12:43:40.829652", "modified": "2023-07-19 13:07:41.753556",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Top Suppliers", "name": "Top Suppliers",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Purchase Receipt Trends", "report_name": "Purchase Receipt Trends",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -66,8 +66,6 @@ frappe.ui.form.on("Supplier", {
}, },
refresh: function (frm) { refresh: function (frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Supplier' }
if (frappe.defaults.get_default("supp_master_name") != "Naming Series") { if (frappe.defaults.get_default("supp_master_name") != "Naming Series") {
frm.toggle_display("naming_series", false); frm.toggle_display("naming_series", false);
} else { } else {

View File

@ -61,7 +61,7 @@
"fieldname": "communication_channel", "fieldname": "communication_channel",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Communication Channel", "label": "Communication Channel",
"options": "\nExotel" "options": ""
} }
], ],
"links": [], "links": [],

View File

@ -822,6 +822,15 @@ def get_purchase_invoices(doctype, txt, searchfield, start, page_len, filters):
return frappe.db.sql(query, filters) return frappe.db.sql(query, filters)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_doctypes_for_closing(doctype, txt, searchfield, start, page_len, filters):
doctypes = frappe.get_hooks("period_closing_doctypes")
if txt:
doctypes = [d for d in doctypes if txt.lower() in d.lower()]
return [(d,) for d in set(doctypes)]
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters): def get_tax_template(doctype, txt, searchfield, start, page_len, filters):

View File

@ -669,7 +669,11 @@ def get_filters(
if reference_voucher_detail_no: if reference_voucher_detail_no:
filters["voucher_detail_no"] = reference_voucher_detail_no filters["voucher_detail_no"] = reference_voucher_detail_no
if item_row and item_row.get("warehouse"): if (
voucher_type in ["Purchase Receipt", "Purchase Invoice"]
and item_row
and item_row.get("warehouse")
):
filters["warehouse"] = item_row.get("warehouse") filters["warehouse"] = item_row.get("warehouse")
return filters return filters

View File

@ -201,6 +201,12 @@ class StockController(AccountsController):
warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"] warehouse_asset_account = warehouse_account[item_row.get("warehouse")]["account"]
expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account") expense_account = frappe.get_cached_value("Company", self.company, "default_expense_account")
if not expense_account:
frappe.throw(
_(
"Please set default cost of goods sold account in company {0} for booking rounding gain and loss during stock transfer"
).format(frappe.bold(self.company))
)
gl_list.append( gl_list.append(
self.get_gl_dict( self.get_gl_dict(

View File

@ -30,11 +30,6 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
var me = this; var me = this;
let doc = this.frm.doc; let doc = this.frm.doc;
erpnext.toggle_naming_series(); erpnext.toggle_naming_series();
frappe.dynamic_link = {
doc: doc,
fieldname: 'name',
doctype: 'Lead'
};
if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) { if (!this.frm.is_new() && doc.__onload && !doc.__onload.is_customer) {
this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create")); this.frm.add_custom_button(__("Customer"), this.make_customer, __("Create"));

View File

@ -3,8 +3,6 @@
frappe.ui.form.on('Prospect', { frappe.ui.form.on('Prospect', {
refresh (frm) { refresh (frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: "name", doctype: frm.doctype };
if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) { if (!frm.is_new() && frappe.boot.user.can_create.includes("Customer")) {
frm.add_custom_button(__("Customer"), function() { frm.add_custom_button(__("Customer"), function() {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({

View File

@ -1,89 +0,0 @@
{
"actions": [],
"creation": "2019-05-21 07:41:53.536536",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"enabled",
"section_break_2",
"account_sid",
"api_key",
"api_token",
"section_break_6",
"map_custom_field_to_doctype",
"target_doctype"
],
"fields": [
{
"default": "0",
"fieldname": "enabled",
"fieldtype": "Check",
"label": "Enabled"
},
{
"depends_on": "enabled",
"fieldname": "section_break_2",
"fieldtype": "Section Break",
"label": "Credentials"
},
{
"fieldname": "account_sid",
"fieldtype": "Data",
"label": "Account SID"
},
{
"fieldname": "api_token",
"fieldtype": "Data",
"label": "API Token"
},
{
"fieldname": "api_key",
"fieldtype": "Data",
"label": "API Key"
},
{
"depends_on": "enabled",
"fieldname": "section_break_6",
"fieldtype": "Section Break",
"label": "Custom Field"
},
{
"default": "0",
"fieldname": "map_custom_field_to_doctype",
"fieldtype": "Check",
"label": "Map Custom Field to DocType"
},
{
"depends_on": "map_custom_field_to_doctype",
"fieldname": "target_doctype",
"fieldtype": "Link",
"label": "Target DocType",
"mandatory_depends_on": "map_custom_field_to_doctype",
"options": "DocType"
}
],
"issingle": 1,
"links": [],
"modified": "2022-12-14 17:24:50.176107",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Exotel Settings",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "ASC",
"states": [],
"track_changes": 1
}

View File

@ -1,22 +0,0 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
import requests
from frappe import _
from frappe.model.document import Document
class ExotelSettings(Document):
def validate(self):
self.verify_credentials()
def verify_credentials(self):
if self.enabled:
response = requests.get(
"https://api.exotel.com/v1/Accounts/{sid}".format(sid=self.account_sid),
auth=(self.api_key, self.api_token),
)
if response.status_code != 200:
frappe.throw(_("Invalid credentials"))

View File

@ -1,151 +0,0 @@
import frappe
import requests
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_incoming_call
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_end_call
# api/method/erpnext.erpnext_integrations.exotel_integration.handle_missed_call
@frappe.whitelist(allow_guest=True)
def handle_incoming_call(**kwargs):
try:
exotel_settings = get_exotel_settings()
if not exotel_settings.enabled:
return
call_payload = kwargs
status = call_payload.get("Status")
if status == "free":
return
call_log = get_call_log(call_payload)
if not call_log:
create_call_log(call_payload)
else:
update_call_log(call_payload, call_log=call_log)
except Exception as e:
frappe.db.rollback()
exotel_settings.log_error("Error in Exotel incoming call")
frappe.db.commit()
@frappe.whitelist(allow_guest=True)
def handle_end_call(**kwargs):
update_call_log(kwargs, "Completed")
@frappe.whitelist(allow_guest=True)
def handle_missed_call(**kwargs):
status = ""
call_type = kwargs.get("CallType")
dial_call_status = kwargs.get("DialCallStatus")
if call_type == "incomplete" and dial_call_status == "no-answer":
status = "No Answer"
elif call_type == "client-hangup" and dial_call_status == "canceled":
status = "Canceled"
elif call_type == "incomplete" and dial_call_status == "failed":
status = "Failed"
update_call_log(kwargs, status)
def update_call_log(call_payload, status="Ringing", call_log=None):
call_log = call_log or get_call_log(call_payload)
# for a new sid, call_log and get_call_log will be empty so create a new log
if not call_log:
call_log = create_call_log(call_payload)
if call_log:
call_log.status = status
call_log.to = call_payload.get("DialWhomNumber")
call_log.duration = call_payload.get("DialCallDuration") or 0
call_log.recording_url = call_payload.get("RecordingUrl")
call_log.save(ignore_permissions=True)
frappe.db.commit()
return call_log
def get_call_log(call_payload):
call_log_id = call_payload.get("CallSid")
if frappe.db.exists("Call Log", call_log_id):
return frappe.get_doc("Call Log", call_log_id)
def map_custom_field(call_payload, call_log):
field_value = call_payload.get("CustomField")
if not field_value:
return call_log
settings = get_exotel_settings()
target_doctype = settings.target_doctype
mapping_enabled = settings.map_custom_field_to_doctype
if not mapping_enabled or not target_doctype:
return call_log
call_log.append("links", {"link_doctype": target_doctype, "link_name": field_value})
return call_log
def create_call_log(call_payload):
call_log = frappe.new_doc("Call Log")
call_log.id = call_payload.get("CallSid")
call_log.to = call_payload.get("DialWhomNumber")
call_log.medium = call_payload.get("To")
call_log.status = "Ringing"
setattr(call_log, "from", call_payload.get("CallFrom"))
map_custom_field(call_payload, call_log)
call_log.save(ignore_permissions=True)
frappe.db.commit()
return call_log
@frappe.whitelist()
def get_call_status(call_id):
endpoint = get_exotel_endpoint("Calls/{call_id}.json".format(call_id=call_id))
response = requests.get(endpoint)
status = response.json().get("Call", {}).get("Status")
return status
@frappe.whitelist()
def make_a_call(from_number, to_number, caller_id, **kwargs):
endpoint = get_exotel_endpoint("Calls/connect.json?details=true")
response = requests.post(
endpoint, data={"From": from_number, "To": to_number, "CallerId": caller_id, **kwargs}
)
return response.json()
def get_exotel_settings():
return frappe.get_single("Exotel Settings")
def whitelist_numbers(numbers, caller_id):
endpoint = get_exotel_endpoint("CustomerWhitelist")
response = requests.post(
endpoint,
data={
"VirtualNumber": caller_id,
"Number": numbers,
},
)
return response
def get_all_exophones():
endpoint = get_exotel_endpoint("IncomingPhoneNumbers")
response = requests.post(endpoint)
return response
def get_exotel_endpoint(action):
settings = get_exotel_settings()
return "https://{api_key}:{api_token}@api.exotel.com/v1/Accounts/{sid}/{action}".format(
api_key=settings.api_key, api_token=settings.api_token, sid=settings.account_sid, action=action
)

View File

@ -230,17 +230,6 @@
"onboard": 0, "onboard": 0,
"type": "Card Break" "type": "Card Break"
}, },
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Exotel Settings",
"link_count": 0,
"link_to": "Exotel Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{ {
"hidden": 0, "hidden": 0,
"is_query_report": 0, "is_query_report": 0,
@ -252,7 +241,7 @@
"type": "Link" "type": "Link"
} }
], ],
"modified": "2023-05-24 14:47:25.984717", "modified": "2023-05-24 14:47:26.984717",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "ERPNext Integrations", "module": "ERPNext Integrations",
"name": "ERPNext Integrations", "name": "ERPNext Integrations",

View File

@ -83,7 +83,7 @@ update_website_context = [
my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context" my_account_context = "erpnext.e_commerce.shopping_cart.utils.update_my_account_context"
webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context" webform_list_context = "erpnext.controllers.website_list_for_contact.get_webform_list_context"
calendars = ["Task", "Work Order", "Leave Application", "Sales Order", "Holiday List", "ToDo"] calendars = ["Task", "Work Order", "Sales Order", "Holiday List", "ToDo"]
website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"] website_generators = ["Item Group", "Website Item", "BOM", "Sales Partner"]
@ -285,10 +285,34 @@ standard_queries = {
"Customer": "erpnext.controllers.queries.customer_query", "Customer": "erpnext.controllers.queries.customer_query",
} }
period_closing_doctypes = [
"Sales Invoice",
"Purchase Invoice",
"Journal Entry",
"Bank Clearance",
"Stock Entry",
"Dunning",
"Invoice Discounting",
"Payment Entry",
"Period Closing Voucher",
"Process Deferred Accounting",
"Asset",
"Asset Capitalization",
"Asset Repair",
"Delivery Note",
"Landed Cost Voucher",
"Purchase Receipt",
"Stock Reconciliation",
"Subcontracting Receipt",
]
doc_events = { doc_events = {
"*": { "*": {
"validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply", "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
}, },
tuple(period_closing_doctypes): {
"validate": "erpnext.accounts.doctype.accounting_period.accounting_period.validate_accounting_period_on_doc_save",
},
"Stock Entry": { "Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty", "on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
@ -334,6 +358,7 @@ doc_events = {
"erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status",
"erpnext.accounts.doctype.dunning.dunning.resolve_dunning", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning",
], ],
"on_cancel": ["erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
"on_trash": "erpnext.regional.check_deletion_permission", "on_trash": "erpnext.regional.check_deletion_permission",
}, },
"Address": { "Address": {
@ -354,6 +379,11 @@ doc_events = {
}, },
} }
# function should expect the variable and doc as arguments
naming_series_variables = {
"FY": "erpnext.accounts.utils.parse_naming_series_variable",
}
# On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled. # On cancel event Payment Entry will be exempted and all linked submittable doctype will get cancelled.
# to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled. # to maintain data integrity we exempted payment entry. it will un-link when sales invoice get cancelled.
# if payment entry not in auto cancel exempted doctypes it will cancel payment entry. # if payment entry not in auto cancel exempted doctypes it will cancel payment entry.
@ -459,15 +489,6 @@ advance_payment_doctypes = ["Sales Order", "Purchase Order"]
invoice_doctypes = ["Sales Invoice", "Purchase Invoice"] invoice_doctypes = ["Sales Invoice", "Purchase Invoice"]
period_closing_doctypes = [
"Sales Invoice",
"Purchase Invoice",
"Journal Entry",
"Bank Clearance",
"Asset",
"Stock Entry",
]
bank_reconciliation_doctypes = [ bank_reconciliation_doctypes = [
"Payment Entry", "Payment Entry",
"Journal Entry", "Journal Entry",
@ -611,3 +632,8 @@ global_search_doctypes = {
additional_timeline_content = { additional_timeline_content = {
"*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"] "*": ["erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs"]
} }
extend_bootinfo = [
"erpnext.support.doctype.service_level_agreement.service_level_agreement.add_sla_doctypes",
]

View File

@ -621,7 +621,7 @@ class ProductionPlan(Document):
def create_work_order(self, item): def create_work_order(self, item):
from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError from erpnext.manufacturing.doctype.work_order.work_order import OverProductionError
if item.get("qty") <= 0: if flt(item.get("qty")) <= 0:
return return
wo = frappe.new_doc("Work Order") wo = frappe.new_doc("Work Order")
@ -697,10 +697,9 @@ class ProductionPlan(Document):
material_request.flags.ignore_permissions = 1 material_request.flags.ignore_permissions = 1
material_request.run_method("set_missing_values") material_request.run_method("set_missing_values")
material_request.save()
if self.get("submit_material_request"): if self.get("submit_material_request"):
material_request.submit() material_request.submit()
else:
material_request.save()
frappe.flags.mute_messages = False frappe.flags.mute_messages = False
@ -1540,7 +1539,7 @@ def get_reserved_qty_for_production_plan(item_code, warehouse):
frappe.qb.from_(table) frappe.qb.from_(table)
.inner_join(child) .inner_join(child)
.on(table.name == child.parent) .on(table.name == child.parent)
.select(Sum(child.required_bom_qty * IfNull(child.conversion_factor, 1.0))) .select(Sum(child.quantity * IfNull(child.conversion_factor, 1.0)))
.where( .where(
(table.docstatus == 1) (table.docstatus == 1)
& (child.item_code == item_code) & (child.item_code == item_code)

View File

@ -933,6 +933,54 @@ class TestProductionPlan(FrappeTestCase):
self.assertEqual(after_qty, before_qty) self.assertEqual(after_qty, before_qty)
def test_resered_qty_for_production_plan_for_material_requests_with_multi_UOM(self):
from erpnext.stock.utils import get_or_make_bin
fg_item = make_item(properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1"}).name
bom_item = make_item(
properties={"is_stock_item": 1, "stock_uom": "_Test UOM 1", "purchase_uom": "Nos"}
).name
if not frappe.db.exists("UOM Conversion Detail", {"parent": bom_item, "uom": "Nos"}):
doc = frappe.get_doc("Item", bom_item)
doc.append("uoms", {"uom": "Nos", "conversion_factor": 25})
doc.save()
make_bom(item=fg_item, raw_materials=[bom_item], source_warehouse="_Test Warehouse - _TC")
bin_name = get_or_make_bin(bom_item, "_Test Warehouse - _TC")
before_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
pln = create_production_plan(
item_code=fg_item, planned_qty=100, ignore_existing_ordered_qty=1, stock_uom="_Test UOM 1"
)
for row in pln.mr_items:
self.assertEqual(row.uom, "Nos")
self.assertEqual(row.quantity, 4)
reserved_qty = flt(frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan"))
self.assertEqual(reserved_qty - before_qty, 100.0)
pln.submit_material_request = 1
pln.make_work_order()
for work_order in frappe.get_all(
"Work Order",
fields=["name"],
filters={"production_plan": pln.name},
):
wo_doc = frappe.get_doc("Work Order", work_order.name)
wo_doc.source_warehouse = "_Test Warehouse - _TC"
wo_doc.wip_warehouse = "_Test Warehouse 1 - _TC"
wo_doc.fg_warehouse = "_Test Warehouse - _TC"
wo_doc.submit()
reserved_qty_after_mr = flt(
frappe.db.get_value("Bin", bin_name, "reserved_qty_for_production_plan")
)
self.assertEqual(reserved_qty_after_mr, before_qty)
def test_skip_available_qty_for_sub_assembly_items(self): def test_skip_available_qty_for_sub_assembly_items(self):
from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom

View File

@ -1026,7 +1026,7 @@ class WorkOrder(Document):
consumed_qty = frappe.db.sql( consumed_qty = frappe.db.sql(
""" """
SELECT SELECT
SUM(qty) SUM(detail.qty)
FROM FROM
`tabStock Entry` entry, `tabStock Entry` entry,
`tabStock Entry Detail` detail `tabStock Entry Detail` detail

View File

@ -17,7 +17,7 @@ frappe.query_reports["Job Card Summary"] = {
label: __("Fiscal Year"), label: __("Fiscal Year"),
fieldtype: "Link", fieldtype: "Link",
options: "Fiscal Year", options: "Fiscal Year",
default: frappe.defaults.get_user_default("fiscal_year"), default: erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
reqd: 1, reqd: 1,
on_change: function(query_report) { on_change: function(query_report) {
var fiscal_year = query_report.get_values().fiscal_year; var fiscal_year = query_report.get_values().fiscal_year;

View File

@ -317,7 +317,7 @@ erpnext.patches.v13_0.update_docs_link
erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries erpnext.patches.v15_0.update_asset_value_for_manual_depr_entries
erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch erpnext.patches.v15_0.update_gpa_and_ndb_for_assdeprsch
erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance erpnext.patches.v14_0.create_accounting_dimensions_for_closing_balance
erpnext.patches.v14_0.update_closing_balances #17-05-2023 erpnext.patches.v14_0.update_closing_balances #14-07-2023
execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0) execute:frappe.db.set_single_value("Accounts Settings", "merge_similar_account_heads", 0)
# below migration patches should always run last # below migration patches should always run last
erpnext.patches.v14_0.migrate_gl_to_payment_ledger erpnext.patches.v14_0.migrate_gl_to_payment_ledger
@ -334,3 +334,6 @@ erpnext.patches.v14_0.cleanup_workspaces
erpnext.patches.v15_0.remove_loan_management_module #2023-07-03 erpnext.patches.v15_0.remove_loan_management_module #2023-07-03
erpnext.patches.v14_0.set_report_in_process_SOA erpnext.patches.v14_0.set_report_in_process_SOA
erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users erpnext.buying.doctype.supplier.patches.migrate_supplier_portal_users
execute:frappe.defaults.clear_default("fiscal_year")
erpnext.patches.v15_0.remove_exotel_integration
erpnext.patches.v14_0.single_to_multi_dunning

View File

@ -0,0 +1,49 @@
import frappe
from erpnext.accounts.general_ledger import make_reverse_gl_entries
def execute():
frappe.reload_doc("accounts", "doctype", "overdue_payment")
frappe.reload_doc("accounts", "doctype", "dunning")
all_dunnings = frappe.get_all("Dunning", filters={"docstatus": ("!=", 2)}, pluck="name")
for dunning_name in all_dunnings:
dunning = frappe.get_doc("Dunning", dunning_name)
if not dunning.sales_invoice:
# nothing we can do
continue
if dunning.overdue_payments:
# something's already here, doesn't need patching
continue
payment_schedules = frappe.get_all(
"Payment Schedule",
filters={"parent": dunning.sales_invoice},
fields=[
"parent as sales_invoice",
"name as payment_schedule",
"payment_term",
"due_date",
"invoice_portion",
"payment_amount",
# at the time of creating this dunning, the full amount was outstanding
"payment_amount as outstanding",
"'0' as paid_amount",
"discounted_amount",
],
)
dunning.extend("overdue_payments", payment_schedules)
dunning.validate()
dunning.flags.ignore_validate_update_after_submit = True
dunning.save()
if dunning.status != "Resolved":
# With the new logic, dunning amount gets recorded as additional income
# at time of payment. We don't want to record the dunning amount twice,
# so we reverse previous GL Entries that recorded the dunning amount at
# time of submission of the Dunning.
make_reverse_gl_entries(voucher_type="Dunning", voucher_no=dunning.name)

View File

@ -13,56 +13,62 @@ from erpnext.accounts.utils import get_fiscal_year
def execute(): def execute():
frappe.db.truncate("Account Closing Balance") frappe.db.truncate("Account Closing Balance")
i = 0 for company in frappe.get_all("Company", pluck="name"):
company_wise_order = {} i = 0
for pcv in frappe.db.get_all( company_wise_order = {}
"Period Closing Voucher", for pcv in frappe.db.get_all(
fields=["company", "posting_date", "name"], "Period Closing Voucher",
filters={"docstatus": 1}, fields=["company", "posting_date", "name"],
order_by="posting_date", filters={"docstatus": 1, "company": company},
): order_by="posting_date",
):
company_wise_order.setdefault(pcv.company, []) company_wise_order.setdefault(pcv.company, [])
if pcv.posting_date not in company_wise_order[pcv.company]: if pcv.posting_date not in company_wise_order[pcv.company]:
pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name) pcv_doc = frappe.get_doc("Period Closing Voucher", pcv.name)
pcv_doc.year_start_date = get_fiscal_year( pcv_doc.year_start_date = get_fiscal_year(
pcv.posting_date, pcv.fiscal_year, company=pcv.company pcv.posting_date, pcv.fiscal_year, company=pcv.company
)[1] )[1]
# get gl entries against pcv # get gl entries against pcv
gl_entries = frappe.db.get_all( gl_entries = frappe.db.get_all(
"GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"] "GL Entry", filters={"voucher_no": pcv.name, "is_cancelled": 0}, fields=["*"]
)
for entry in gl_entries:
entry["is_period_closing_voucher_entry"] = 1
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
# get all gl entries for the year
closing_entries = frappe.db.get_all(
"GL Entry",
filters={
"is_cancelled": 0,
"voucher_no": ["!=", pcv.name],
"posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]],
"is_opening": "No",
},
fields=["*"],
)
if i == 0:
# add opening entries only for the first pcv
closing_entries += frappe.db.get_all(
"GL Entry",
filters={"is_cancelled": 0, "is_opening": "Yes"},
fields=["*"],
) )
for entry in gl_entries:
entry["is_period_closing_voucher_entry"] = 1
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
for entry in closing_entries: closing_entries = []
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
make_closing_entries(gl_entries + closing_entries, voucher_name=pcv.name) if pcv.posting_date not in company_wise_order[pcv.company]:
company_wise_order[pcv.company].append(pcv.posting_date) # get all gl entries for the year
closing_entries = frappe.db.get_all(
"GL Entry",
filters={
"is_cancelled": 0,
"voucher_no": ["!=", pcv.name],
"posting_date": ["between", [pcv_doc.year_start_date, pcv.posting_date]],
"is_opening": "No",
"company": company,
},
fields=["*"],
)
i += 1 if i == 0:
# add opening entries only for the first pcv
closing_entries += frappe.db.get_all(
"GL Entry",
filters={"is_cancelled": 0, "is_opening": "Yes", "company": company},
fields=["*"],
)
for entry in closing_entries:
entry["closing_date"] = pcv_doc.posting_date
entry["period_closing_voucher"] = pcv_doc.name
entries = gl_entries + closing_entries
make_closing_entries(entries, pcv.name, pcv.company, pcv.posting_date)
company_wise_order[pcv.company].append(pcv.posting_date)
i += 1

View File

@ -0,0 +1,37 @@
import click
import frappe
from frappe import _
from frappe.desk.doctype.notification_log.notification_log import make_notification_logs
from frappe.utils.user import get_system_managers
SETTINGS_DOCTYPE = "Exotel Settings"
def execute():
if "exotel_integration" in frappe.get_installed_apps():
return
try:
exotel = frappe.get_doc(SETTINGS_DOCTYPE)
if exotel.enabled:
notify_existing_users()
frappe.delete_doc("DocType", SETTINGS_DOCTYPE)
except Exception:
frappe.log_error("Failed to remove Exotel Integration.")
def notify_existing_users():
click.secho(
"Exotel integration is moved to a separate app and will be removed from ERPNext in version-15.\n"
"Please install the app to continue using the integration: https://github.com/frappe/exotel_integration",
fg="yellow",
)
notification = {
"subject": _(
"WARNING: Exotel app has been separated from ERPNext, please install the app to continue using Exotel integration."
),
"type": "Alert",
}
make_notification_logs(notification, get_system_managers(only_name=True))

View File

@ -358,12 +358,14 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
} }
refresh() { refresh() {
erpnext.toggle_naming_series(); erpnext.toggle_naming_series();
erpnext.hide_company(); erpnext.hide_company();
this.set_dynamic_labels(); this.set_dynamic_labels();
this.setup_sms(); this.setup_sms();
this.setup_quality_inspection(); this.setup_quality_inspection();
this.validate_has_items(); this.validate_has_items();
erpnext.utils.view_serial_batch_nos(this.frm);
} }
scan_barcode() { scan_barcode() {

View File

@ -56,7 +56,7 @@ erpnext.financial_statements = {
// dropdown for links to other financial statements // dropdown for links to other financial statements
erpnext.financial_statements.filters = get_filters() erpnext.financial_statements.filters = get_filters()
let fiscal_year = frappe.defaults.get_user_default("fiscal_year") let fiscal_year = erpnext.utils.get_fiscal_year(frappe.datetime.get_today());
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
@ -137,7 +137,7 @@ function get_filters() {
"label": __("Start Year"), "label": __("Start Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'"
}, },
@ -146,7 +146,7 @@ function get_filters() {
"label": __("End Year"), "label": __("End Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"depends_on": "eval:doc.filter_based_on == 'Fiscal Year'" "depends_on": "eval:doc.filter_based_on == 'Fiscal Year'"
}, },
@ -182,6 +182,16 @@ function get_filters() {
company: frappe.query_report.get_filter_value("company") company: frappe.query_report.get_filter_value("company")
}); });
} }
},
{
"fieldname": "project",
"label": __("Project"),
"fieldtype": "MultiSelectList",
get_data: function(txt) {
return frappe.db.get_link_options('Project', txt, {
company: frappe.query_report.get_filter_value("company")
});
},
} }
] ]

View File

@ -113,6 +113,23 @@ $.extend(erpnext.utils, {
} }
}, },
view_serial_batch_nos: function(frm) {
let bundle_ids = frm.doc.items.filter(d => d.serial_and_batch_bundle);
if (bundle_ids?.length) {
frm.add_custom_button(__('Serial / Batch Nos'), () => {
frappe.route_options = {
"voucher_no": frm.doc.name,
"voucher_type": frm.doc.doctype,
"from_date": frm.doc.posting_date || frm.doc.transaction_date,
"to_date": frm.doc.posting_date || frm.doc.transaction_date,
"company": frm.doc.company,
};
frappe.set_route("query-report", "Serial and Batch Summary");
}, __('View'));
}
},
add_indicator_for_multicompany: function(frm, info) { add_indicator_for_multicompany: function(frm, info) {
frm.dashboard.stats_area.show(); frm.dashboard.stats_area.show();
frm.dashboard.stats_area_row.addClass('flex'); frm.dashboard.stats_area_row.addClass('flex');
@ -381,6 +398,27 @@ $.extend(erpnext.utils, {
}); });
}); });
}); });
},
get_fiscal_year: function(date) {
if(!date) {
date = frappe.datetime.get_today();
}
let fiscal_year = '';
frappe.call({
method: "erpnext.accounts.utils.get_fiscal_year",
args: {
date: date
},
async: false,
callback: function(r) {
if (r.message) {
fiscal_year = r.message[0];
}
}
});
return fiscal_year;
} }
}); });
@ -632,7 +670,6 @@ erpnext.utils.update_child_items = function(opts) {
fields.splice(3, 0, { fields.splice(3, 0, {
fieldtype: 'Float', fieldtype: 'Float',
fieldname: "conversion_factor", fieldname: "conversion_factor",
in_list_view: 1,
label: __("Conversion Factor"), label: __("Conversion Factor"),
precision: get_precision('conversion_factor') precision: get_precision('conversion_factor')
}) })
@ -640,6 +677,7 @@ erpnext.utils.update_child_items = function(opts) {
new frappe.ui.Dialog({ new frappe.ui.Dialog({
title: __("Update Items"), title: __("Update Items"),
size: "extra-large",
fields: [ fields: [
{ {
fieldname: "trans_items", fieldname: "trans_items",
@ -854,95 +892,87 @@ $(document).on('app_ready', function() {
// Show SLA dashboard // Show SLA dashboard
$(document).on('app_ready', function() { $(document).on('app_ready', function() {
frappe.call({ $.each(frappe.boot.service_level_agreement_doctypes, function(_i, d) {
method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_sla_doctypes', frappe.ui.form.on(d, {
callback: function(r) { onload: function(frm) {
if (!r.message) if (!frm.doc.service_level_agreement)
return; return;
$.each(r.message, function(_i, d) { frappe.call({
frappe.ui.form.on(d, { method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters',
onload: function(frm) { args: {
if (!frm.doc.service_level_agreement) doctype: frm.doc.doctype,
return; name: frm.doc.service_level_agreement,
customer: frm.doc.customer
frappe.call({
method: 'erpnext.support.doctype.service_level_agreement.service_level_agreement.get_service_level_agreement_filters',
args: {
doctype: frm.doc.doctype,
name: frm.doc.service_level_agreement,
customer: frm.doc.customer
},
callback: function (r) {
if (r && r.message) {
frm.set_query('priority', function() {
return {
filters: {
'name': ['in', r.message.priority],
}
};
});
frm.set_query('service_level_agreement', function() {
return {
filters: {
'name': ['in', r.message.service_level_agreements],
}
};
});
}
}
});
}, },
callback: function (r) {
refresh: function(frm) { if (r && r.message) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement frm.set_query('priority', function() {
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) { return {
frappe.call({ filters: {
'method': 'frappe.client.get', 'name': ['in', r.message.priority],
args: {
doctype: 'Service Level Agreement',
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution);
} }
} };
});
frm.set_query('service_level_agreement', function() {
return {
filters: {
'name': ['in', r.message.service_level_agreements],
}
};
}); });
} else if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ?
{'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} :
{'indicator': 'red', 'msg': 'Service Level Agreement Failed'};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} }
}, }
}); });
}); },
}
refresh: function(frm) {
if (frm.doc.status !== 'Closed' && frm.doc.service_level_agreement
&& ['First Response Due', 'Resolution Due'].includes(frm.doc.agreement_status)) {
frappe.call({
'method': 'frappe.client.get',
args: {
doctype: 'Service Level Agreement',
name: frm.doc.service_level_agreement
},
callback: function(data) {
let statuses = data.message.pause_sla_on;
const hold_statuses = [];
$.each(statuses, (_i, entry) => {
hold_statuses.push(entry.status);
});
if (hold_statuses.includes(frm.doc.status)) {
frm.dashboard.clear_headline();
let message = {'indicator': 'orange', 'msg': __('SLA is on hold since {0}', [moment(frm.doc.on_hold_since).fromNow(true)])};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ message.indicator +'"><span>'+ message.msg +'</span></span> ' +
'</div>' +
'</div>'
);
} else {
set_time_to_resolve_and_response(frm, data.message.apply_sla_for_resolution);
}
}
});
} else if (frm.doc.service_level_agreement) {
frm.dashboard.clear_headline();
let agreement_status = (frm.doc.agreement_status == 'Fulfilled') ?
{'indicator': 'green', 'msg': 'Service Level Agreement has been fulfilled'} :
{'indicator': 'red', 'msg': 'Service Level Agreement Failed'};
frm.dashboard.set_headline_alert(
'<div class="row">' +
'<div class="col-xs-12">' +
'<span class="indicator whitespace-nowrap '+ agreement_status.indicator +'"><span class="hidden-xs">'+ agreement_status.msg +'</span></span> ' +
'</div>' +
'</div>'
);
}
},
});
}); });
}); });

View File

@ -16,7 +16,7 @@ frappe.query_reports["Fichier des Ecritures Comptables [FEC]"] = {
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1 "reqd": 1
} }
], ],

View File

@ -17,7 +17,7 @@ frappe.query_reports["IRS 1099"] = {
"label": __("Fiscal Year"), "label": __("Fiscal Year"),
"fieldtype": "Link", "fieldtype": "Link",
"options": "Fiscal Year", "options": "Fiscal Year",
"default": frappe.defaults.get_user_default("fiscal_year"), "default": erpnext.utils.get_fiscal_year(frappe.datetime.get_today()),
"reqd": 1, "reqd": 1,
"width": 80, "width": 80,
}, },

View File

@ -5,18 +5,19 @@
"custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}", "custom_options": "{\"type\": \"line\", \"axisOptions\": {\"shortenYAxisNumbers\": 1}, \"tooltipOptions\": {}, \"lineOptions\": {\"regionFill\": 1}}",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Monthly\",\"based_on\":\"Item\"}", "filters_json": "{\"period\":\"Monthly\",\"based_on\":\"Item\"}",
"idx": 0, "idx": 1,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 16:24:45.726270", "modified": "2023-07-19 13:09:45.341791",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order Trends", "name": "Sales Order Trends",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Sales Order Trends", "report_name": "Sales Order Trends",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Line", "type": "Line",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -5,18 +5,19 @@
"custom_options": "", "custom_options": "",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}",
"filters_json": "{\"period\":\"Yearly\",\"based_on\":\"Customer\"}", "filters_json": "{\"period\":\"Yearly\",\"based_on\":\"Customer\"}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2020-07-22 17:03:10.320147", "modified": "2023-07-19 13:14:20.151502",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Top Customers", "name": "Top Customers",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Delivery Note Trends", "report_name": "Delivery Note Trends",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@ -131,8 +131,6 @@ frappe.ui.form.on("Customer", {
erpnext.toggle_naming_series(); erpnext.toggle_naming_series();
} }
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Customer'}
if(!frm.doc.__islocal) { if(!frm.doc.__islocal) {
frappe.contacts.render_address_and_contact(frm); frappe.contacts.render_address_and_contact(frm);

View File

@ -1904,12 +1904,11 @@ class TestSalesOrder(FrappeTestCase):
"voucher_no": so.name, "voucher_no": so.name,
"voucher_detail_no": item.name, "voucher_detail_no": item.name,
}, },
fields=["status", "reserved_qty", "delivered_qty"], fields=["reserved_qty", "delivered_qty"],
) )
for sre_detail in sre_details: for sre_detail in sre_details:
self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty) self.assertEqual(sre_detail.reserved_qty, sre_detail.delivered_qty)
self.assertEqual(sre_detail.status, "Delivered")
def test_delivered_item_material_request(self): def test_delivered_item_material_request(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."

View File

@ -13,7 +13,7 @@ frappe.query_reports["Address And Contacts"] = {
"get_query": function() { "get_query": function() {
return { return {
"filters": { "filters": {
"name": ["in","Customer,Supplier,Sales Partner"], "name": ["in","Customer,Supplier,Sales Partner,Lead"],
} }
} }
} }

View File

@ -130,6 +130,7 @@ def get_party_group(party_type):
"Customer": "customer_group", "Customer": "customer_group",
"Supplier": "supplier_group", "Supplier": "supplier_group",
"Sales Partner": "partner_type", "Sales Partner": "partner_type",
"Lead": "status",
} }
return group[party_type] return group[party_type]

View File

@ -81,8 +81,6 @@ frappe.ui.form.on("Company", {
disbale_coa_fields(frm); disbale_coa_fields(frm);
frappe.contacts.render_address_and_contact(frm); frappe.contacts.render_address_and_contact(frm);
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Company'}
if (frappe.perm.has_perm("Cost Center", 0, 'read')) { if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
frm.add_custom_button(__('Cost Centers'), function() { frm.add_custom_button(__('Cost Centers'), function() {
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name}); frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});

View File

@ -1,352 +1,99 @@
{ {
"actions": [],
"allow_copy": 1, "allow_copy": 1,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2013-05-02 17:53:24", "creation": "2013-05-02 17:53:24",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 0, "engine": "InnoDB",
"field_order": [
"default_company",
"country",
"default_distance_unit",
"column_break_8",
"default_currency",
"hide_currency_symbol",
"disable_rounded_total",
"disable_in_words"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "default_company", "fieldname": "default_company",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Company", "label": "Default Company",
"length": 0, "options": "Company"
"no_copy": 0,
"options": "Company",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "current_fiscal_year",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Current Fiscal Year",
"length": 0,
"no_copy": 0,
"options": "Fiscal Year",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "country", "fieldname": "country",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Country", "label": "Country",
"length": 0, "options": "Country"
"no_copy": 0,
"options": "Country",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "",
"fieldname": "default_distance_unit", "fieldname": "default_distance_unit",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Default Distance Unit", "label": "Default Distance Unit",
"length": 0, "options": "UOM"
"no_copy": 0,
"options": "UOM",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "INR", "default": "INR",
"fieldname": "default_currency", "fieldname": "default_currency",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 1, "ignore_user_permissions": 1,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Default Currency", "label": "Default Currency",
"length": 0,
"no_copy": 0,
"options": "Currency", "options": "Currency",
"permlevel": 0, "reqd": 1
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "Do not show any symbol like $ etc next to currencies.", "description": "Do not show any symbol like $ etc next to currencies.",
"fieldname": "hide_currency_symbol", "fieldname": "hide_currency_symbol",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Hide Currency Symbol", "label": "Hide Currency Symbol",
"length": 0, "options": "\nNo\nYes"
"no_copy": 0,
"options": "\nNo\nYes",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "If disable, 'Rounded Total' field will not be visible in any transaction", "description": "If disable, 'Rounded Total' field will not be visible in any transaction",
"fieldname": "disable_rounded_total", "fieldname": "disable_rounded_total",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Disable Rounded Total"
"label": "Disable Rounded Total",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "If disable, 'In Words' field will not be visible in any transaction", "description": "If disable, 'In Words' field will not be visible in any transaction",
"fieldname": "disable_in_words", "fieldname": "disable_in_words",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Disable In Words"
"label": "Disable In Words",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-cog", "icon": "fa fa-cog",
"idx": 1, "idx": 1,
"image_view": 0,
"in_create": 1, "in_create": 1,
"is_submittable": 0,
"issingle": 1, "issingle": 1,
"istable": 0, "links": [],
"max_attachments": 0, "modified": "2023-07-01 19:45:00.323953",
"menu_index": 0,
"modified": "2018-10-15 03:08:19.886212",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Global Defaults", "name": "Global Defaults",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 0,
"email": 0,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 0,
"read": 1, "read": 1,
"report": 0,
"role": "System Manager", "role": "System Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 1, "read_only": 1,
"read_only_onload": 0, "sort_field": "modified",
"show_name_in_global_search": 0,
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": []
"track_seen": 0,
"track_views": 0
} }

View File

@ -10,7 +10,6 @@ from frappe.utils import cint
keydict = { keydict = {
# "key in defaults": "key in Global Defaults" # "key in defaults": "key in Global Defaults"
"fiscal_year": "current_fiscal_year",
"company": "default_company", "company": "default_company",
"currency": "default_currency", "currency": "default_currency",
"country": "country", "country": "country",
@ -29,22 +28,6 @@ class GlobalDefaults(Document):
for key in keydict: for key in keydict:
frappe.db.set_default(key, self.get(keydict[key], "")) frappe.db.set_default(key, self.get(keydict[key], ""))
# update year start date and year end date from fiscal_year
if self.current_fiscal_year:
if fiscal_year := frappe.get_all(
"Fiscal Year",
filters={"name": self.current_fiscal_year},
fields=["year_start_date", "year_end_date"],
limit=1,
order_by=None,
):
ysd = fiscal_year[0].year_start_date or ""
yed = fiscal_year[0].year_end_date or ""
if ysd and yed:
frappe.db.set_default("year_start_date", ysd.strftime("%Y-%m-%d"))
frappe.db.set_default("year_end_date", yed.strftime("%Y-%m-%d"))
# enable default currency # enable default currency
if self.default_currency: if self.default_currency:
frappe.db.set_value("Currency", self.default_currency, "enabled", 1) frappe.db.set_value("Currency", self.default_currency, "enabled", 1)

View File

@ -6,13 +6,41 @@ frappe.ui.form.on("Holiday List", {
if (frm.doc.holidays) { if (frm.doc.holidays) {
frm.set_value("total_holidays", frm.doc.holidays.length); frm.set_value("total_holidays", frm.doc.holidays.length);
} }
frm.call("get_supported_countries").then(r => {
frm.subdivisions_by_country = r.message.subdivisions_by_country;
frm.fields_dict.country.set_data(
r.message.countries.sort((a, b) => a.label.localeCompare(b.label))
);
if (frm.doc.country) {
frm.trigger("set_subdivisions");
}
});
}, },
from_date: function(frm) { from_date: function(frm) {
if (frm.doc.from_date && !frm.doc.to_date) { if (frm.doc.from_date && !frm.doc.to_date) {
var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12); var a_year_from_start = frappe.datetime.add_months(frm.doc.from_date, 12);
frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1)); frm.set_value("to_date", frappe.datetime.add_days(a_year_from_start, -1));
} }
} },
country: function(frm) {
frm.set_value("subdivision", "");
if (frm.doc.country) {
frm.trigger("set_subdivisions");
}
},
set_subdivisions: function(frm) {
const subdivisions = [...frm.subdivisions_by_country[frm.doc.country]];
if (subdivisions && subdivisions.length > 0) {
frm.fields_dict.subdivision.set_data(subdivisions);
frm.set_df_property("subdivision", "hidden", 0);
} else {
frm.fields_dict.subdivision.set_data([]);
frm.set_df_property("subdivision", "hidden", 1);
}
},
}); });
frappe.tour["Holiday List"] = [ frappe.tour["Holiday List"] = [

View File

@ -1,480 +1,166 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0,
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:holiday_list_name", "autoname": "field:holiday_list_name",
"beta": 0,
"creation": "2013-01-10 16:34:14", "creation": "2013-01-10 16:34:14",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"editable_grid": 0,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [
"holiday_list_name",
"from_date",
"to_date",
"column_break_4",
"total_holidays",
"add_weekly_holidays",
"weekly_off",
"get_weekly_off_dates",
"add_local_holidays",
"country",
"subdivision",
"get_local_holidays",
"holidays_section",
"holidays",
"clear_table",
"section_break_9",
"color"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holiday_list_name", "fieldname": "holiday_list_name",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holiday List Name", "label": "Holiday List Name",
"length": 0,
"no_copy": 0,
"oldfieldname": "holiday_list_name", "oldfieldname": "holiday_list_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 1 "unique": 1
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "from_date", "fieldname": "from_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "From Date", "label": "From Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "to_date", "fieldname": "to_date",
"fieldtype": "Date", "fieldtype": "Date",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "To Date", "label": "To Date",
"length": 0, "reqd": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_4", "fieldname": "column_break_4",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_holidays", "fieldname": "total_holidays",
"fieldtype": "Int", "fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Total Holidays", "label": "Total Holidays",
"length": 0, "read_only": 1
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 1, "collapsible": 1,
"columns": 0, "depends_on": "eval: doc.from_date && doc.to_date",
"fieldname": "add_weekly_holidays", "fieldname": "add_weekly_holidays",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Add Weekly Holidays"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Add Weekly Holidays",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "weekly_off", "fieldname": "weekly_off",
"fieldtype": "Select", "fieldtype": "Select",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Weekly Off", "label": "Weekly Off",
"length": 0,
"no_copy": 1, "no_copy": 1,
"options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday", "options": "\nSunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
"permlevel": 0,
"print_hide": 1, "print_hide": 1,
"print_hide_if_no_value": 0, "report_hide": 1
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 1,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "get_weekly_off_dates", "fieldname": "get_weekly_off_dates",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Add to Holidays", "label": "Add to Holidays",
"length": 0, "options": "get_weekly_off_dates"
"no_copy": 0,
"options": "get_weekly_off_dates",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holidays_section", "fieldname": "holidays_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hidden": 0, "label": "Holidays"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holidays",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "holidays", "fieldname": "holidays",
"fieldtype": "Table", "fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Holidays", "label": "Holidays",
"length": 0,
"no_copy": 0,
"oldfieldname": "holiday_list_details", "oldfieldname": "holiday_list_details",
"oldfieldtype": "Table", "oldfieldtype": "Table",
"options": "Holiday", "options": "Holiday"
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "clear_table", "fieldname": "clear_table",
"fieldtype": "Button", "fieldtype": "Button",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Clear Table", "label": "Clear Table",
"length": 0, "options": "clear_table"
"no_copy": 0,
"options": "clear_table",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "color", "fieldname": "color",
"fieldtype": "Color", "fieldtype": "Color",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Color", "label": "Color",
"length": 0, "print_hide": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "country",
"print_hide": 1, "fieldtype": "Autocomplete",
"print_hide_if_no_value": 0, "label": "Country"
"read_only": 0, },
"remember_last_selected_value": 0, {
"report_hide": 0, "depends_on": "country",
"reqd": 0, "fieldname": "subdivision",
"search_index": 0, "fieldtype": "Autocomplete",
"set_only_once": 0, "label": "Subdivision"
"translatable": 0, },
"unique": 0 {
"collapsible": 1,
"depends_on": "eval: doc.from_date && doc.to_date",
"fieldname": "add_local_holidays",
"fieldtype": "Section Break",
"label": "Add Local Holidays"
},
{
"fieldname": "get_local_holidays",
"fieldtype": "Button",
"label": "Add to Holidays",
"options": "get_local_holidays"
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "fa fa-calendar", "icon": "fa fa-calendar",
"idx": 1, "idx": 1,
"image_view": 0, "links": [],
"in_create": 0, "modified": "2023-07-14 13:28:53.156421",
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-07-03 07:22:46.474096",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Holiday List", "name": "Holiday List",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "HR Manager", "role": "HR Manager",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": []
"track_seen": 0,
"track_views": 0
} }

View File

@ -3,11 +3,15 @@
import json import json
from datetime import date
import frappe import frappe
from babel import Locale
from frappe import _, throw from frappe import _, throw
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, formatdate, getdate, today from frappe.utils import formatdate, getdate, today
from holidays import country_holidays
from holidays.utils import list_supported_countries
class OverlapError(frappe.ValidationError): class OverlapError(frappe.ValidationError):
@ -21,25 +25,66 @@ class HolidayList(Document):
@frappe.whitelist() @frappe.whitelist()
def get_weekly_off_dates(self): def get_weekly_off_dates(self):
self.validate_values()
date_list = self.get_weekly_off_date_list(self.from_date, self.to_date)
last_idx = max(
[cint(d.idx) for d in self.get("holidays")]
or [
0,
]
)
for i, d in enumerate(date_list):
ch = self.append("holidays", {})
ch.description = _(self.weekly_off)
ch.holiday_date = d
ch.weekly_off = 1
ch.idx = last_idx + i + 1
def validate_values(self):
if not self.weekly_off: if not self.weekly_off:
throw(_("Please select weekly off day")) throw(_("Please select weekly off day"))
existing_holidays = self.get_holidays()
for d in self.get_weekly_off_date_list(self.from_date, self.to_date):
if d in existing_holidays:
continue
self.append("holidays", {"description": _(self.weekly_off), "holiday_date": d, "weekly_off": 1})
self.sort_holidays()
@frappe.whitelist()
def get_supported_countries(self):
subdivisions_by_country = list_supported_countries()
countries = [
{"value": country, "label": local_country_name(country)}
for country in subdivisions_by_country.keys()
]
return {
"countries": countries,
"subdivisions_by_country": subdivisions_by_country,
}
@frappe.whitelist()
def get_local_holidays(self):
if not self.country:
throw(_("Please select a country"))
existing_holidays = self.get_holidays()
from_date = getdate(self.from_date)
to_date = getdate(self.to_date)
for holiday_date, holiday_name in country_holidays(
self.country,
subdiv=self.subdivision,
years=[from_date.year, to_date.year],
language=frappe.local.lang,
).items():
if holiday_date in existing_holidays:
continue
if holiday_date < from_date or holiday_date > to_date:
continue
self.append(
"holidays", {"description": holiday_name, "holiday_date": holiday_date, "weekly_off": 0}
)
self.sort_holidays()
def sort_holidays(self):
self.holidays.sort(key=lambda x: getdate(x.holiday_date))
for i in range(len(self.holidays)):
self.holidays[i].idx = i + 1
def get_holidays(self) -> list[date]:
return [getdate(holiday.holiday_date) for holiday in self.holidays]
def validate_days(self): def validate_days(self):
if getdate(self.from_date) > getdate(self.to_date): if getdate(self.from_date) > getdate(self.to_date):
throw(_("To Date cannot be before From Date")) throw(_("To Date cannot be before From Date"))
@ -120,3 +165,8 @@ def is_holiday(holiday_list, date=None):
) )
else: else:
return False return False
def local_country_name(country_code: str) -> str:
"""Return the localized country name for the given country code."""
return Locale.parse(frappe.local.lang).territories.get(country_code, country_code)

View File

@ -3,7 +3,7 @@
import unittest import unittest
from contextlib import contextmanager from contextlib import contextmanager
from datetime import timedelta from datetime import date, timedelta
import frappe import frappe
from frappe.utils import getdate from frappe.utils import getdate
@ -23,6 +23,41 @@ class TestHolidayList(unittest.TestCase):
fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name) fetched_holiday_list = frappe.get_value("Holiday List", holiday_list.name)
self.assertEqual(holiday_list.name, fetched_holiday_list) self.assertEqual(holiday_list.name, fetched_holiday_list)
def test_weekly_off(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-01-01"
holiday_list.to_date = "2023-02-28"
holiday_list.weekly_off = "Sunday"
holiday_list.get_weekly_off_dates()
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
self.assertNotIn(date(2022, 12, 25), holidays)
self.assertIn(date(2023, 1, 1), holidays)
self.assertIn(date(2023, 1, 8), holidays)
self.assertIn(date(2023, 1, 15), holidays)
self.assertIn(date(2023, 1, 22), holidays)
self.assertIn(date(2023, 1, 29), holidays)
self.assertIn(date(2023, 2, 5), holidays)
self.assertIn(date(2023, 2, 12), holidays)
self.assertIn(date(2023, 2, 19), holidays)
self.assertIn(date(2023, 2, 26), holidays)
self.assertNotIn(date(2023, 3, 5), holidays)
def test_local_holidays(self):
holiday_list = frappe.new_doc("Holiday List")
holiday_list.from_date = "2023-04-01"
holiday_list.to_date = "2023-04-30"
holiday_list.country = "DE"
holiday_list.subdivision = "SN"
holiday_list.get_local_holidays()
holidays = [holiday.holiday_date for holiday in holiday_list.holidays]
self.assertNotIn(date(2023, 1, 1), holidays)
self.assertIn(date(2023, 4, 7), holidays)
self.assertIn(date(2023, 4, 10), holidays)
self.assertNotIn(date(2023, 5, 1), holidays)
def make_holiday_list( def make_holiday_list(
name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None name, from_date=getdate() - timedelta(days=10), to_date=getdate(), holiday_dates=None

View File

@ -3,8 +3,6 @@
frappe.ui.form.on('Sales Partner', { frappe.ui.form.on('Sales Partner', {
refresh: function(frm) { refresh: function(frm) {
frappe.dynamic_link = {doc: frm.doc, fieldname: 'name', doctype: 'Sales Partner'}
if(frm.doc.__islocal){ if(frm.doc.__islocal){
hide_field(['address_html', 'contact_html', 'address_contacts']); hide_field(['address_html', 'contact_html', 'address_contacts']);
frappe.contacts.clear_address_and_contact(frm); frappe.contacts.clear_address_and_contact(frm);

View File

@ -462,11 +462,9 @@ def install_defaults(args=None): # nosemgrep
def set_global_defaults(args): def set_global_defaults(args):
global_defaults = frappe.get_doc("Global Defaults", "Global Defaults") global_defaults = frappe.get_doc("Global Defaults", "Global Defaults")
current_fiscal_year = frappe.get_all("Fiscal Year")[0]
global_defaults.update( global_defaults.update(
{ {
"current_fiscal_year": current_fiscal_year.name,
"default_currency": args.get("currency"), "default_currency": args.get("currency"),
"default_company": args.get("company_name"), "default_company": args.get("company_name"),
"country": args.get("country"), "country": args.get("country"),

View File

@ -318,6 +318,37 @@ class TestDeliveryNote(FrappeTestCase):
self.assertEqual(dn.per_returned, 100) self.assertEqual(dn.per_returned, 100)
self.assertEqual(dn.status, "Return Issued") self.assertEqual(dn.status, "Return Issued")
def test_delivery_note_return_valuation_on_different_warehuose(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")
item_code = "Test Return Valuation For DN"
make_item("Test Return Valuation For DN", {"is_stock_item": 1})
return_warehouse = create_warehouse("Returned Test Warehouse", company=company)
make_stock_entry(item_code=item_code, target="Stores - TCP1", qty=5, basic_rate=150)
dn = create_delivery_note(
item_code=item_code,
qty=5,
rate=500,
warehouse="Stores - TCP1",
company=company,
expense_account="Cost of Goods Sold - TCP1",
cost_center="Main - TCP1",
)
dn.submit()
self.assertEqual(dn.items[0].incoming_rate, 150)
from erpnext.controllers.sales_and_purchase_return import make_return_doc
return_dn = make_return_doc(dn.doctype, dn.name)
return_dn.items[0].warehouse = return_warehouse
return_dn.save().submit()
self.assertEqual(return_dn.items[0].incoming_rate, 150)
def test_return_single_item_from_bundled_items(self): def test_return_single_item_from_bundled_items(self):
company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company") company = frappe.db.get_value("Warehouse", "Stores - TCP1", "company")

View File

@ -194,7 +194,8 @@
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled",
"search_index": 1
}, },
{ {
"default": "0", "default": "0",
@ -911,7 +912,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"make_attachments_public": 1, "make_attachments_public": 1,
"modified": "2023-02-14 04:48:26.343620", "modified": "2023-07-14 17:18:18.658942",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item", "name": "Item",

View File

@ -773,7 +773,7 @@ class Item(Document):
rows = "" rows = ""
for docname, attr_list in not_included.items(): for docname, attr_list in not_included.items():
link = "<a href='/app/Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname))) link = f"<a href='/app/item/{docname}'>{frappe.bold(docname)}</a>"
rows += table_row(link, body(attr_list)) rows += table_row(link, body(attr_list))
error_description = _( error_description = _(

View File

@ -1,370 +1,90 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "",
"beta": 0,
"creation": "2015-05-19 05:12:30.344797", "creation": "2015-05-19 05:12:30.344797",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "Other", "document_type": "Other",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"variant_of",
"attribute",
"column_break_2",
"attribute_value",
"numeric_values",
"section_break_4",
"from_range",
"increment",
"column_break_8",
"to_range"
],
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "variant_of", "fieldname": "variant_of",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Variant Of", "label": "Variant Of",
"length": 0,
"no_copy": 0,
"options": "Item", "options": "Item",
"permlevel": 0, "search_index": 1
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "attribute", "fieldname": "attribute",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Attribute", "label": "Attribute",
"length": 0,
"no_copy": 0,
"options": "Item Attribute", "options": "Item Attribute",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1, "reqd": 1,
"search_index": 0, "search_index": 1
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "attribute_value", "fieldname": "attribute_value",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0, "label": "Attribute Value"
"label": "Attribute Value",
"length": 0,
"no_copy": 0,
"options": "",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0, "default": "0",
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "has_variants", "depends_on": "has_variants",
"fieldname": "numeric_values", "fieldname": "numeric_values",
"fieldtype": "Check", "fieldtype": "Check",
"hidden": 0, "label": "Numeric Values"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Numeric Values",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "numeric_values", "depends_on": "numeric_values",
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "from_range", "fieldname": "from_range",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "From Range"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "From Range",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "increment", "fieldname": "increment",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "Increment"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Increment",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break", "fieldtype": "Column Break"
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"depends_on": "",
"fieldname": "to_range", "fieldname": "to_range",
"fieldtype": "Float", "fieldtype": "Float",
"hidden": 0, "label": "To Range"
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "To Range",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"icon": "",
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1, "istable": 1,
"max_attachments": 0, "links": [],
"modified": "2019-01-03 15:36:59.129006", "modified": "2023-07-14 17:15:19.112119",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Item Variant Attribute", "name": "Item Variant Attribute",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 0, "states": []
"track_seen": 0,
"track_views": 0
} }

View File

@ -3,7 +3,6 @@
frappe.ui.form.on('Manufacturer', { frappe.ui.form.on('Manufacturer', {
refresh: function(frm) { refresh: function(frm) {
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Manufacturer' };
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
hide_field(['address_html','contact_html']); hide_field(['address_html','contact_html']);
frappe.contacts.clear_address_and_contact(frm); frappe.contacts.clear_address_and_contact(frm);

View File

@ -118,8 +118,8 @@ class MaterialRequest(BuyingController):
self.title = _("{0} Request for {1}").format(_(self.material_request_type), items)[:100] self.title = _("{0} Request for {1}").format(_(self.material_request_type), items)[:100]
def on_submit(self): def on_submit(self):
self.update_requested_qty()
self.update_requested_qty_in_production_plan() self.update_requested_qty_in_production_plan()
self.update_requested_qty()
if self.material_request_type == "Purchase": if self.material_request_type == "Purchase":
self.validate_budget() self.validate_budget()
@ -178,8 +178,8 @@ class MaterialRequest(BuyingController):
) )
def on_cancel(self): def on_cancel(self):
self.update_requested_qty()
self.update_requested_qty_in_production_plan() self.update_requested_qty_in_production_plan()
self.update_requested_qty()
def get_mr_items_ordered_qty(self, mr_items): def get_mr_items_ordered_qty(self, mr_items):
mr_items_ordered_qty = {} mr_items_ordered_qty = {}
@ -270,7 +270,13 @@ class MaterialRequest(BuyingController):
item_wh_list.append([d.item_code, d.warehouse]) item_wh_list.append([d.item_code, d.warehouse])
for item_code, warehouse in item_wh_list: for item_code, warehouse in item_wh_list:
update_bin_qty(item_code, warehouse, {"indented_qty": get_indented_qty(item_code, warehouse)}) update_bin_qty(
item_code,
warehouse,
{
"indented_qty": get_indented_qty(item_code, warehouse),
},
)
def update_requested_qty_in_production_plan(self): def update_requested_qty_in_production_plan(self):
production_plans = [] production_plans = []

View File

@ -1965,6 +1965,32 @@ class TestPurchaseReceipt(FrappeTestCase):
ste5.reload() ste5.reload()
self.assertEqual(ste5.items[0].valuation_rate, 275.00) self.assertEqual(ste5.items[0].valuation_rate, 275.00)
ste6 = make_stock_entry(
purpose="Material Transfer",
posting_date=add_days(today(), -3),
source=warehouse1,
target=warehouse,
item_code=item_code,
qty=20,
company=pr.company,
)
ste6.reload()
self.assertEqual(ste6.items[0].valuation_rate, 275.00)
ste7 = make_stock_entry(
purpose="Material Transfer",
posting_date=add_days(today(), -3),
source=warehouse,
target=warehouse1,
item_code=item_code,
qty=20,
company=pr.company,
)
ste7.reload()
self.assertEqual(ste7.items[0].valuation_rate, 275.00)
create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1) create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=2500 * -1)
pr.reload() pr.reload()
@ -1985,6 +2011,12 @@ class TestPurchaseReceipt(FrappeTestCase):
ste5.reload() ste5.reload()
self.assertEqual(ste5.items[0].valuation_rate, valuation_rate) self.assertEqual(ste5.items[0].valuation_rate, valuation_rate)
ste6.reload()
self.assertEqual(ste6.items[0].valuation_rate, valuation_rate)
ste7.reload()
self.assertEqual(ste7.items[0].valuation_rate, valuation_rate)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

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