Merge branch 'develop' into alternative-items-quotation

This commit is contained in:
Marica 2023-02-15 13:05:33 +05:30 committed by GitHub
commit 22010b745c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
57 changed files with 1118 additions and 472 deletions

View File

@ -4,7 +4,7 @@
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007 erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar

View File

@ -40,7 +40,7 @@ class Dunning(AccountsController):
def on_cancel(self): def on_cancel(self):
if self.dunning_amount: if self.dunning_amount:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def make_gl_entries(self): def make_gl_entries(self):

View File

@ -234,7 +234,7 @@ class PaymentReconciliation(Document):
def allocate_entries(self, args): def allocate_entries(self, args):
self.validate_entries() self.validate_entries()
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices")) invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
default_exchange_gain_loss_account = frappe.get_cached_value( default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account" "Company", self.company, "exchange_gain_loss_account"
) )
@ -253,6 +253,9 @@ class PaymentReconciliation(Document):
pay["amount"] = 0 pay["amount"] = 0
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number")) inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name"))
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate") res.exchange_rate = inv.get("exchange_rate")
@ -407,13 +410,21 @@ class PaymentReconciliation(Document):
if not self.get("payments"): if not self.get("payments"):
frappe.throw(_("No records found in the Payments table")) frappe.throw(_("No records found in the Payments table"))
def get_invoice_exchange_map(self, invoices): def get_invoice_exchange_map(self, invoices, payments):
sales_invoices = [ sales_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice" d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
] ]
sales_invoices.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"]
)
purchase_invoices = [ purchase_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice" d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
] ]
purchase_invoices.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"]
)
invoice_exchange_map = frappe._dict() invoice_exchange_map = frappe._dict()
if sales_invoices: if sales_invoices:

View File

@ -473,6 +473,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")] invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
pr.get_unreconciled_entries() pr.get_unreconciled_entries()
@ -506,6 +511,11 @@ class TestPaymentReconciliation(FrappeTestCase):
payments = [x.as_dict() for x in pr.get("payments")] payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = allocated_amount pr.allocation[0].allocated_amount = allocated_amount
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile() pr.reconcile()
# assert outstanding # assert outstanding

View File

@ -45,21 +45,20 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required")) frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self): def validate_payment_request_amount(self):
existing_payment_request_amount = get_existing_payment_request_amount( existing_payment_request_amount = flt(
self.reference_doctype, self.reference_name get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
) )
if existing_payment_request_amount: ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart": ref_amount = get_amount(ref_doc, self.payment_account)
ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount: if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw( frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format( _("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype self.reference_doctype
)
) )
)
def validate_currency(self): def validate_currency(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name) ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)

View File

@ -135,6 +135,34 @@ def get_assets(filters):
where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != '' where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s and ads.asset = a.name and ads.docstatus=1 and ads.name = ds.parent and ifnull(ds.journal_entry, '') != ''
group by a.asset_category group by a.asset_category
union union
SELECT a.asset_category,
ifnull(sum(case when gle.posting_date < %(from_date)s and (ifnull(a.disposal_date, 0) = 0 or a.disposal_date >= %(from_date)s) then
gle.debit
else
0
end), 0) as accumulated_depreciation_as_on_from_date,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and a.disposal_date >= %(from_date)s
and a.disposal_date <= %(to_date)s and gle.posting_date <= a.disposal_date then
gle.debit
else
0
end), 0) as depreciation_eliminated_during_the_period,
ifnull(sum(case when gle.posting_date >= %(from_date)s and gle.posting_date <= %(to_date)s
and (ifnull(a.disposal_date, 0) = 0 or gle.posting_date <= a.disposal_date) then
gle.debit
else
0
end), 0) as depreciation_amount_during_the_period
from `tabGL Entry` gle
join `tabAsset` a on
gle.against_voucher = a.name
join `tabAsset Category Account` aca on
aca.parent = a.asset_category and aca.company_name = %(company)s
join `tabCompany` company on
company.name = %(company)s
where a.docstatus=1 and a.company=%(company)s and a.calculate_depreciation=0 and a.purchase_date <= %(to_date)s and gle.debit != 0 and gle.is_cancelled = 0 and gle.account = ifnull(aca.depreciation_expense_account, company.depreciation_expense_account)
group by a.asset_category
union
SELECT a.asset_category, SELECT a.asset_category,
ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then ifnull(sum(case when ifnull(a.disposal_date, 0) != 0 and (a.disposal_date < %(from_date)s or a.disposal_date > %(to_date)s) then
0 0

View File

@ -1512,9 +1512,12 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa
ref_doc = frappe.get_doc(voucher_type, voucher_no) ref_doc = frappe.get_doc(voucher_type, voucher_no)
# Didn't use db_set for optimisation purpose # Didn't use db_set for optimisation purpose
ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] ref_doc.outstanding_amount = outstanding["outstanding_in_account_currency"] or 0.0
frappe.db.set_value( frappe.db.set_value(
voucher_type, voucher_no, "outstanding_amount", outstanding["outstanding_in_account_currency"] voucher_type,
voucher_no,
"outstanding_amount",
outstanding["outstanding_in_account_currency"] or 0.0,
) )
ref_doc.set_status(update=True) ref_doc.set_status(update=True)

View File

@ -209,62 +209,62 @@ frappe.ui.form.on('Asset', {
return return
} }
var x_intervals = [frm.doc.purchase_date]; var x_intervals = [frappe.format(frm.doc.purchase_date, { fieldtype: 'Date' })];
var asset_values = [frm.doc.gross_purchase_amount]; var asset_values = [frm.doc.gross_purchase_amount];
var last_depreciation_date = frm.doc.purchase_date;
if(frm.doc.opening_accumulated_depreciation) {
last_depreciation_date = frappe.datetime.add_months(frm.doc.next_depreciation_date,
-1*frm.doc.frequency_of_depreciation);
x_intervals.push(last_depreciation_date);
asset_values.push(flt(frm.doc.gross_purchase_amount) -
flt(frm.doc.opening_accumulated_depreciation));
}
if(frm.doc.calculate_depreciation) { if(frm.doc.calculate_depreciation) {
if (frm.doc.finance_books.length == 1) { if(frm.doc.opening_accumulated_depreciation) {
let depr_schedule = (await frappe.call( var depreciation_date = frappe.datetime.add_months(
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule", frm.doc.finance_books[0].depreciation_start_date,
{ -1 * frm.doc.finance_books[0].frequency_of_depreciation
asset_name: frm.doc.name, );
status: frm.doc.docstatus ? "Active" : "Draft", x_intervals.push(frappe.format(depreciation_date, { fieldtype: 'Date' }));
finance_book: frm.doc.finance_books[0].finance_book || null asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
}
)).message;
$.each(depr_schedule || [], function(i, v) {
x_intervals.push(v.schedule_date);
var asset_value = flt(frm.doc.gross_purchase_amount) - flt(v.accumulated_depreciation_amount);
if(v.journal_entry) {
last_depreciation_date = v.schedule_date;
asset_values.push(asset_value);
} else {
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
asset_values.push(null);
} else {
asset_values.push(asset_value)
}
}
});
} }
let depr_schedule = (await frappe.call(
"erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule.get_depr_schedule",
{
asset_name: frm.doc.name,
status: frm.doc.docstatus ? "Active" : "Draft",
finance_book: frm.doc.finance_books[0].finance_book || null
}
)).message;
$.each(depr_schedule || [], function(i, v) {
x_intervals.push(frappe.format(v.schedule_date, { fieldtype: 'Date' }));
var asset_value = flt(frm.doc.gross_purchase_amount - v.accumulated_depreciation_amount, precision('gross_purchase_amount'));
if(v.journal_entry) {
asset_values.push(asset_value);
} else {
if (in_list(["Scrapped", "Sold"], frm.doc.status)) {
asset_values.push(null);
} else {
asset_values.push(asset_value)
}
}
});
} else { } else {
if(frm.doc.opening_accumulated_depreciation) {
x_intervals.push(frappe.format(frm.doc.creation.split(" ")[0], { fieldtype: 'Date' }));
asset_values.push(flt(frm.doc.gross_purchase_amount - frm.doc.opening_accumulated_depreciation, precision('gross_purchase_amount')));
}
let depr_entries = (await frappe.call({ let depr_entries = (await frappe.call({
method: "get_manual_depreciation_entries", method: "get_manual_depreciation_entries",
doc: frm.doc, doc: frm.doc,
})).message; })).message;
$.each(depr_entries || [], function(i, v) { $.each(depr_entries || [], function(i, v) {
x_intervals.push(v.posting_date); x_intervals.push(frappe.format(v.posting_date, { fieldtype: 'Date' }));
last_depreciation_date = v.posting_date;
let last_asset_value = asset_values[asset_values.length - 1] let last_asset_value = asset_values[asset_values.length - 1]
asset_values.push(last_asset_value - v.value); asset_values.push(last_asset_value - v.value);
}); });
} }
if(in_list(["Scrapped", "Sold"], frm.doc.status)) { if(in_list(["Scrapped", "Sold"], frm.doc.status)) {
x_intervals.push(frm.doc.disposal_date); x_intervals.push(frappe.format(frm.doc.disposal_date, { fieldtype: 'Date' }));
asset_values.push(0); asset_values.push(0);
last_depreciation_date = frm.doc.disposal_date;
} }
frm.dashboard.render_graph({ frm.dashboard.render_graph({

View File

@ -429,25 +429,16 @@ class Asset(AccountsController):
def get_value_after_depreciation(self, finance_book=None): def get_value_after_depreciation(self, finance_book=None):
if not self.calculate_depreciation: if not self.calculate_depreciation:
return self.value_after_depreciation return flt(self.value_after_depreciation, self.precision("gross_purchase_amount"))
if not finance_book: if not finance_book:
return self.get("finance_books")[0].value_after_depreciation return flt(
self.get("finance_books")[0].value_after_depreciation, self.precision("gross_purchase_amount")
)
for row in self.get("finance_books"): for row in self.get("finance_books"):
if finance_book == row.finance_book: if finance_book == row.finance_book:
return row.value_after_depreciation return flt(row.value_after_depreciation, self.precision("gross_purchase_amount"))
def _get_value_after_depreciation_for_making_schedule(self, fb_row):
# value_after_depreciation - current Asset value
if self.docstatus == 1 and fb_row.value_after_depreciation:
value_after_depreciation = flt(fb_row.value_after_depreciation)
else:
value_after_depreciation = flt(self.gross_purchase_amount) - flt(
self.opening_accumulated_depreciation
)
return value_after_depreciation
def get_default_finance_book_idx(self): def get_default_finance_book_idx(self):
if not self.get("default_finance_book") and self.company: if not self.get("default_finance_book") and self.company:

View File

@ -134,7 +134,7 @@ class AssetDepreciationSchedule(Document):
): ):
asset_doc.validate_asset_finance_books(row) asset_doc.validate_asset_finance_books(row)
value_after_depreciation = asset_doc._get_value_after_depreciation_for_making_schedule(row) value_after_depreciation = _get_value_after_depreciation_for_making_schedule(asset_doc, row)
row.value_after_depreciation = value_after_depreciation row.value_after_depreciation = value_after_depreciation
if update_asset_finance_book_row: if update_asset_finance_book_row:
@ -325,6 +325,17 @@ class AssetDepreciationSchedule(Document):
) )
def _get_value_after_depreciation_for_making_schedule(asset_doc, fb_row):
if asset_doc.docstatus == 1 and fb_row.value_after_depreciation:
value_after_depreciation = flt(fb_row.value_after_depreciation)
else:
value_after_depreciation = flt(asset_doc.gross_purchase_amount) - flt(
asset_doc.opening_accumulated_depreciation
)
return value_after_depreciation
def make_draft_asset_depr_schedules_if_not_present(asset_doc): def make_draft_asset_depr_schedules_if_not_present(asset_doc):
for row in asset_doc.get("finance_books"): for row in asset_doc.get("finance_books"):
draft_asset_depr_schedule_name = get_asset_depr_schedule_name( draft_asset_depr_schedule_name = get_asset_depr_schedule_name(

View File

@ -5,7 +5,7 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.utils import cstr, formatdate, getdate from frappe.utils import cstr, flt, formatdate, getdate
from erpnext.accounts.report.financial_statements import ( from erpnext.accounts.report.financial_statements import (
get_fiscal_year_data, get_fiscal_year_data,
@ -102,13 +102,9 @@ def get_data(filters):
] ]
assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields) assets_record = frappe.db.get_all("Asset", filters=conditions, fields=fields)
finance_book_filter = ("is", "not set")
if filters.finance_book:
finance_book_filter = ("=", filters.finance_book)
assets_linked_to_fb = frappe.db.get_all( assets_linked_to_fb = frappe.db.get_all(
doctype="Asset Finance Book", doctype="Asset Finance Book",
filters={"finance_book": finance_book_filter}, filters={"finance_book": filters.finance_book or ("is", "not set")},
pluck="parent", pluck="parent",
) )
@ -194,7 +190,7 @@ def get_depreciation_amount_of_asset(asset, depreciation_amount_map, filters):
else: else:
depr_amount = get_manual_depreciation_amount_of_asset(asset, filters) depr_amount = get_manual_depreciation_amount_of_asset(asset, filters)
return depr_amount return flt(depr_amount, 2)
def get_finance_book_value_map(filters): def get_finance_book_value_map(filters):

View File

@ -124,12 +124,11 @@ frappe.ui.form.on("Request for Quotation",{
frappe.urllib.get_full_url( frappe.urllib.get_full_url(
"/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" + "/api/method/erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_pdf?" +
new URLSearchParams({ new URLSearchParams({
doctype: frm.doc.doctype,
name: frm.doc.name, name: frm.doc.name,
supplier: data.supplier, supplier: data.supplier,
print_format: data.print_format || "Standard", print_format: data.print_format || "Standard",
language: data.language || frappe.boot.lang, language: data.language || frappe.boot.lang,
letter_head: data.letter_head || frm.doc.letter_head || "", letterhead: data.letter_head || frm.doc.letter_head || "",
}).toString() }).toString()
) )
); );

View File

@ -3,6 +3,7 @@
import json import json
from typing import Optional
import frappe import frappe
from frappe import _ from frappe import _
@ -388,24 +389,26 @@ def create_rfq_items(sq_doc, supplier, data):
@frappe.whitelist() @frappe.whitelist()
def get_pdf(doctype, name, supplier, print_format=None, language=None, letter_head=None): def get_pdf(
# permissions get checked in `download_pdf` name: str,
if doc := get_rfq_doc(doctype, name, supplier): supplier: str,
download_pdf( print_format: Optional[str] = None,
doctype, language: Optional[str] = None,
name, letterhead: Optional[str] = None,
print_format, ):
doc=doc, doc = frappe.get_doc("Request for Quotation", name)
language=language,
letter_head=letter_head or None,
)
def get_rfq_doc(doctype, name, supplier):
if supplier: if supplier:
doc = frappe.get_doc(doctype, name)
doc.update_supplier_part_no(supplier) doc.update_supplier_part_no(supplier)
return doc
# permissions get checked in `download_pdf`
download_pdf(
doc.doctype,
doc.name,
print_format,
doc=doc,
language=language,
letterhead=letterhead or None,
)
@frappe.whitelist() @frappe.whitelist()

View File

@ -8,6 +8,7 @@ from frappe.utils import nowdate
from erpnext.buying.doctype.request_for_quotation.request_for_quotation import ( from erpnext.buying.doctype.request_for_quotation.request_for_quotation import (
create_supplier_quotation, create_supplier_quotation,
get_pdf,
make_supplier_quotation_from_rfq, make_supplier_quotation_from_rfq,
) )
from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq
@ -124,6 +125,11 @@ class TestRequestforQuotation(FrappeTestCase):
rfq.status = "Draft" rfq.status = "Draft"
rfq.submit() rfq.submit()
def test_get_pdf(self):
rfq = make_request_for_quotation()
get_pdf(rfq.name, rfq.get("suppliers")[0].supplier)
self.assertEqual(frappe.local.response.type, "pdf")
def make_request_for_quotation(**args): def make_request_for_quotation(**args):
""" """

View File

@ -252,6 +252,7 @@ def get_already_returned_items(doc):
child.parent = par.name and par.docstatus = 1 child.parent = par.name and par.docstatus = 1
and par.is_return = 1 and par.return_against = %s and par.is_return = 1 and par.return_against = %s
group by item_code group by item_code
for update
""".format( """.format(
column, doc.doctype, doc.doctype column, doc.doctype, doc.doctype
), ),

View File

@ -26,10 +26,11 @@
} }
], ],
"links": [], "links": [],
"modified": "2021-02-08 12:51:48.971517", "modified": "2023-02-10 00:51:44.973957",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Lead Source", "name": "Lead Source",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -58,5 +59,7 @@
], ],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC" "sort_order": "DESC",
"states": [],
"translated_doctype": 1
} }

View File

@ -18,10 +18,11 @@
} }
], ],
"links": [], "links": [],
"modified": "2020-05-20 12:22:01.866472", "modified": "2023-02-10 01:40:23.713390",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "CRM", "module": "CRM",
"name": "Sales Stage", "name": "Sales Stage",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -40,5 +41,7 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1 "states": [],
"track_changes": 1,
"translated_doctype": 1
} }

View File

@ -11,6 +11,40 @@ frappe.query_reports["Loan Interest Report"] = {
"options": "Company", "options": "Company",
"default": frappe.defaults.get_user_default("Company"), "default": frappe.defaults.get_user_default("Company"),
"reqd": 1 "reqd": 1
} },
{
"fieldname":"applicant_type",
"label": __("Applicant Type"),
"fieldtype": "Select",
"options": ["Customer", "Employee"],
"reqd": 1,
"default": "Customer",
on_change: function() {
frappe.query_report.set_filter_value('applicant', "");
}
},
{
"fieldname": "applicant",
"label": __("Applicant"),
"fieldtype": "Dynamic Link",
"get_options": function() {
var applicant_type = frappe.query_report.get_filter_value('applicant_type');
var applicant = frappe.query_report.get_filter_value('applicant');
if(applicant && !applicant_type) {
frappe.throw(__("Please select Applicant Type first"));
}
return applicant_type;
}
},
{
"fieldname":"from_date",
"label": __("From Date"),
"fieldtype": "Date",
},
{
"fieldname":"to_date",
"label": __("From Date"),
"fieldtype": "Date",
},
] ]
}; };

View File

@ -13,12 +13,12 @@ from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applic
def execute(filters=None): def execute(filters=None):
columns = get_columns(filters) columns = get_columns()
data = get_active_loan_details(filters) data = get_active_loan_details(filters)
return columns, data return columns, data
def get_columns(filters): def get_columns():
columns = [ columns = [
{"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160}, {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160},
{"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160}, {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160},
@ -70,6 +70,13 @@ def get_columns(filters):
"options": "currency", "options": "currency",
"width": 120, "width": 120,
}, },
{
"label": _("Accrued Principal"),
"fieldname": "accrued_principal",
"fieldtype": "Currency",
"options": "currency",
"width": 120,
},
{ {
"label": _("Total Repayment"), "label": _("Total Repayment"),
"fieldname": "total_repayment", "fieldname": "total_repayment",
@ -137,11 +144,16 @@ def get_columns(filters):
def get_active_loan_details(filters): def get_active_loan_details(filters):
filter_obj = {
filter_obj = {"status": ("!=", "Closed")} "status": ("!=", "Closed"),
"docstatus": 1,
}
if filters.get("company"): if filters.get("company"):
filter_obj.update({"company": filters.get("company")}) filter_obj.update({"company": filters.get("company")})
if filters.get("applicant"):
filter_obj.update({"applicant": filters.get("applicant")})
loan_details = frappe.get_all( loan_details = frappe.get_all(
"Loan", "Loan",
fields=[ fields=[
@ -167,8 +179,8 @@ def get_active_loan_details(filters):
sanctioned_amount_map = get_sanctioned_amount_map() sanctioned_amount_map = get_sanctioned_amount_map()
penal_interest_rate_map = get_penal_interest_rate_map() penal_interest_rate_map = get_penal_interest_rate_map()
payments = get_payments(loan_list) payments = get_payments(loan_list, filters)
accrual_map = get_interest_accruals(loan_list) accrual_map = get_interest_accruals(loan_list, filters)
currency = erpnext.get_company_currency(filters.get("company")) currency = erpnext.get_company_currency(filters.get("company"))
for loan in loan_details: for loan in loan_details:
@ -183,6 +195,7 @@ def get_active_loan_details(filters):
- flt(loan.written_off_amount), - flt(loan.written_off_amount),
"total_repayment": flt(payments.get(loan.loan)), "total_repayment": flt(payments.get(loan.loan)),
"accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")), "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
"accrued_principal": flt(accrual_map.get(loan.loan, {}).get("accrued_principal")),
"interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")), "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
"penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")), "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
"penalty_interest": penal_interest_rate_map.get(loan.loan_type), "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
@ -212,20 +225,35 @@ def get_sanctioned_amount_map():
) )
def get_payments(loans): def get_payments(loans, filters):
query_filters = {"against_loan": ("in", loans)}
if filters.get("from_date"):
query_filters.update({"posting_date": (">=", filters.get("from_date"))})
if filters.get("to_date"):
query_filters.update({"posting_date": ("<=", filters.get("to_date"))})
return frappe._dict( return frappe._dict(
frappe.get_all( frappe.get_all(
"Loan Repayment", "Loan Repayment",
fields=["against_loan", "sum(amount_paid)"], fields=["against_loan", "sum(amount_paid)"],
filters={"against_loan": ("in", loans)}, filters=query_filters,
group_by="against_loan", group_by="against_loan",
as_list=1, as_list=1,
) )
) )
def get_interest_accruals(loans): def get_interest_accruals(loans, filters):
accrual_map = {} accrual_map = {}
query_filters = {"loan": ("in", loans)}
if filters.get("from_date"):
query_filters.update({"posting_date": (">=", filters.get("from_date"))})
if filters.get("to_date"):
query_filters.update({"posting_date": ("<=", filters.get("to_date"))})
interest_accruals = frappe.get_all( interest_accruals = frappe.get_all(
"Loan Interest Accrual", "Loan Interest Accrual",
@ -236,8 +264,9 @@ def get_interest_accruals(loans):
"penalty_amount", "penalty_amount",
"paid_interest_amount", "paid_interest_amount",
"accrual_type", "accrual_type",
"payable_principal_amount",
], ],
filters={"loan": ("in", loans)}, filters=query_filters,
order_by="posting_date desc", order_by="posting_date desc",
) )
@ -246,6 +275,7 @@ def get_interest_accruals(loans):
entry.loan, entry.loan,
{ {
"accrued_interest": 0.0, "accrued_interest": 0.0,
"accrued_principal": 0.0,
"undue_interest": 0.0, "undue_interest": 0.0,
"interest_outstanding": 0.0, "interest_outstanding": 0.0,
"last_accrual_date": "", "last_accrual_date": "",
@ -270,6 +300,7 @@ def get_interest_accruals(loans):
accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount accrual_map[entry.loan]["undue_interest"] += entry.interest_amount - entry.paid_interest_amount
accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount
accrual_map[entry.loan]["accrued_principal"] += entry.payable_principal_amount
if last_accrual_date and getdate(entry.posting_date) == last_accrual_date: if last_accrual_date and getdate(entry.posting_date) == last_accrual_date:
accrual_map[entry.loan]["penalty"] = entry.penalty_amount accrual_map[entry.loan]["penalty"] = entry.penalty_amount

View File

@ -0,0 +1,315 @@
{
"charts": [],
"content": "[{\"id\":\"_38WStznya\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Your Shortcuts</b></span>\",\"col\":12}},{\"id\":\"t7o_K__1jB\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan Application\",\"col\":3}},{\"id\":\"IRiNDC6w1p\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Loan\",\"col\":3}},{\"id\":\"xbbo0FYbq0\",\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"id\":\"7ZL4Bro-Vi\",\"type\":\"spacer\",\"data\":{\"col\":12}},{\"id\":\"yhyioTViZ3\",\"type\":\"header\",\"data\":{\"text\":\"<span class=\\\"h4\\\"><b>Reports &amp; Masters</b></span>\",\"col\":12}},{\"id\":\"oYFn4b1kSw\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan\",\"col\":4}},{\"id\":\"vZepJF5tl9\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Processes\",\"col\":4}},{\"id\":\"k-393Mjhqe\",\"type\":\"card\",\"data\":{\"card_name\":\"Disbursement and Repayment\",\"col\":4}},{\"id\":\"6crJ0DBiBJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Loan Security\",\"col\":4}},{\"id\":\"Um5YwxVLRJ\",\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]",
"creation": "2020-03-12 16:35:55.299820",
"docstatus": 0,
"doctype": "Workspace",
"for_user": "",
"hide_custom": 0,
"icon": "loan",
"idx": 0,
"is_hidden": 0,
"label": "Loans",
"links": [
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Type",
"link_count": 0,
"link_to": "Loan Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Application",
"link_count": 0,
"link_to": "Loan Application",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan",
"link_count": 0,
"link_to": "Loan",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan Processes",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Process Loan Security Shortfall",
"link_count": 0,
"link_to": "Process Loan Security Shortfall",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Process Loan Interest Accrual",
"link_count": 0,
"link_to": "Process Loan Interest Accrual",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Disbursement and Repayment",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Disbursement",
"link_count": 0,
"link_to": "Loan Disbursement",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Repayment",
"link_count": 0,
"link_to": "Loan Repayment",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Write Off",
"link_count": 0,
"link_to": "Loan Write Off",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Interest Accrual",
"link_count": 0,
"link_to": "Loan Interest Accrual",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security",
"link_count": 0,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Type",
"link_count": 0,
"link_to": "Loan Security Type",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Price",
"link_count": 0,
"link_to": "Loan Security Price",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security",
"link_count": 0,
"link_to": "Loan Security",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Pledge",
"link_count": 0,
"link_to": "Loan Security Pledge",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Unpledge",
"link_count": 0,
"link_to": "Loan Security Unpledge",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
"label": "Loan Security Shortfall",
"link_count": 0,
"link_to": "Loan Security Shortfall",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "Reports",
"link_count": 6,
"onboard": 0,
"type": "Card Break"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Loan Repayment and Closure",
"link_count": 0,
"link_to": "Loan Repayment and Closure",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"dependencies": "",
"hidden": 0,
"is_query_report": 1,
"label": "Loan Security Status",
"link_count": 0,
"link_to": "Loan Security Status",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Loan Interest Report",
"link_count": 0,
"link_to": "Loan Interest Report",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Loan Security Exposure",
"link_count": 0,
"link_to": "Loan Security Exposure",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Applicant-Wise Loan Security Exposure",
"link_count": 0,
"link_to": "Applicant-Wise Loan Security Exposure",
"link_type": "Report",
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 1,
"label": "Loan Security Status",
"link_count": 0,
"link_to": "Loan Security Status",
"link_type": "Report",
"onboard": 0,
"type": "Link"
}
],
"modified": "2023-01-31 19:47:13.114415",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loans",
"owner": "Administrator",
"parent_page": "",
"public": 1,
"quick_lists": [],
"restrict_to_domain": "",
"roles": [],
"sequence_id": 16.0,
"shortcuts": [
{
"color": "Green",
"format": "{} Open",
"label": "Loan Application",
"link_to": "Loan Application",
"stats_filter": "{ \"status\": \"Open\" }",
"type": "DocType"
},
{
"label": "Loan",
"link_to": "Loan",
"type": "DocType"
},
{
"doc_view": "",
"label": "Dashboard",
"link_to": "Loan Dashboard",
"type": "Dashboard"
}
],
"title": "Loans"
}

View File

@ -289,7 +289,7 @@
{ {
"fieldname": "scrap_items", "fieldname": "scrap_items",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Items", "label": "Scrap Items",
"options": "BOM Scrap Item" "options": "BOM Scrap Item"
}, },
{ {
@ -605,7 +605,7 @@
"image_field": "image", "image_field": "image",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-01-10 07:47:08.652616", "modified": "2023-02-13 17:31:37.504565",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Manufacturing", "module": "Manufacturing",
"name": "BOM", "name": "BOM",

View File

@ -1,16 +1,17 @@
import frappe import frappe
from frappe import _
def execute(): def execute():
from erpnext.setup.setup_wizard.operations.install_fixtures import default_sales_partner_type from erpnext.setup.setup_wizard.operations.install_fixtures import read_lines
frappe.reload_doc("selling", "doctype", "sales_partner_type") frappe.reload_doc("selling", "doctype", "sales_partner_type")
frappe.local.lang = frappe.db.get_default("lang") or "en" frappe.local.lang = frappe.db.get_default("lang") or "en"
default_sales_partner_type = read_lines("sales_partner_type.txt")
for s in default_sales_partner_type: for s in default_sales_partner_type:
insert_sales_partner_type(_(s)) insert_sales_partner_type(s)
# get partner type in existing forms (customized) # get partner type in existing forms (customized)
# and create a document if not created # and create a document if not created

View File

@ -161,6 +161,37 @@ class TestTimesheet(unittest.TestCase):
to_time = timesheet.time_logs[0].to_time to_time = timesheet.time_logs[0].to_time
self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True)) self.assertEqual(to_time, add_to_date(from_time, hours=2, as_datetime=True))
def test_per_billed_hours(self):
"""If amounts are 0, per_billed should be calculated based on hours."""
ts = frappe.new_doc("Timesheet")
ts.total_billable_amount = 0
ts.total_billed_amount = 0
ts.total_billable_hours = 2
ts.total_billed_hours = 0.5
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 25)
ts.total_billed_hours = 2
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def test_per_billed_amount(self):
"""If amounts are > 0, per_billed should be calculated based on amounts, regardless of hours."""
ts = frappe.new_doc("Timesheet")
ts.total_billable_hours = 2
ts.total_billed_hours = 1
ts.total_billable_amount = 200
ts.total_billed_amount = 50
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 25)
ts.total_billed_hours = 3
ts.total_billable_amount = 200
ts.total_billed_amount = 200
ts.calculate_percentage_billed()
self.assertEqual(ts.per_billed, 100)
def make_timesheet( def make_timesheet(
employee, employee,

View File

@ -64,6 +64,8 @@ class Timesheet(Document):
self.per_billed = 0 self.per_billed = 0
if self.total_billed_amount > 0 and self.total_billable_amount > 0: if self.total_billed_amount > 0 and self.total_billable_amount > 0:
self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount
elif self.total_billed_hours > 0 and self.total_billable_hours > 0:
self.per_billed = (self.total_billed_hours * 100) / self.total_billable_hours
def update_billing_hours(self, args): def update_billing_hours(self, args):
if args.is_billable: if args.is_billable:

View File

@ -129,7 +129,16 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
frappe.model.round_floats_in(item); frappe.model.round_floats_in(item);
item.net_rate = item.rate; item.net_rate = item.rate;
item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty; item.qty = item.qty === undefined ? (me.frm.doc.is_return ? -1 : 1) : item.qty;
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
if (!(me.frm.doc.is_return || me.frm.doc.is_debit_note)) {
item.net_amount = item.amount = flt(item.rate * item.qty, precision("amount", item));
}
else {
let qty = item.qty || 1;
qty = me.frm.doc.is_return ? -1 * qty : qty;
item.net_amount = item.amount = flt(item.rate * qty, precision("amount", item));
}
item.item_tax_amount = 0.0; item.item_tax_amount = 0.0;
item.total_weight = flt(item.weight_per_unit * item.stock_qty); item.total_weight = flt(item.weight_per_unit * item.stock_qty);

View File

@ -1,123 +1,68 @@
{ {
"allow_copy": 0, "actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:industry", "autoname": "field:industry",
"beta": 0, "creation": "2012-03-27 14:36:09",
"creation": "2012-03-27 14:36:09", "doctype": "DocType",
"custom": 0, "document_type": "Setup",
"docstatus": 0, "engine": "InnoDB",
"doctype": "DocType", "field_order": [
"document_type": "Setup", "industry"
"editable_grid": 0, ],
"fields": [ "fields": [
{ {
"allow_on_submit": 0, "fieldname": "industry",
"bold": 0, "fieldtype": "Data",
"collapsible": 0, "in_list_view": 1,
"fieldname": "industry", "label": "Industry",
"fieldtype": "Data", "oldfieldname": "industry",
"hidden": 0, "oldfieldtype": "Data",
"ignore_user_permissions": 0, "reqd": 1,
"ignore_xss_filter": 0, "unique": 1
"in_filter": 0,
"in_list_view": 0,
"label": "Industry",
"length": 0,
"no_copy": 0,
"oldfieldname": "industry",
"oldfieldtype": "Data",
"permlevel": 0,
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
} }
], ],
"hide_heading": 0, "icon": "fa fa-flag",
"hide_toolbar": 0, "idx": 1,
"icon": "fa fa-flag", "links": [],
"idx": 1, "modified": "2023-02-10 03:14:40.735763",
"image_view": 0, "modified_by": "Administrator",
"in_create": 0, "module": "Selling",
"name": "Industry Type",
"is_submittable": 0, "naming_rule": "By fieldname",
"issingle": 0, "owner": "Administrator",
"istable": 0,
"max_attachments": 0,
"modified": "2020-09-18 17:26:09.703215",
"modified_by": "Administrator",
"module": "Selling",
"name": "Industry Type",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "email": 1,
"cancel": 0, "print": 1,
"create": 1, "read": 1,
"delete": 0, "report": 1,
"email": 1, "role": "Sales Manager",
"export": 0, "share": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0, "email": 1,
"apply_user_permissions": 0, "print": 1,
"cancel": 0, "read": 1,
"create": 0, "report": 1,
"delete": 0, "role": "Sales User"
"email": 1, },
"export": 0,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
"set_user_permissions": 0,
"share": 0,
"submit": 0,
"write": 0
},
{ {
"amend": 0, "create": 1,
"apply_user_permissions": 0, "email": 1,
"cancel": 0, "print": 1,
"create": 1, "read": 1,
"delete": 0, "report": 1,
"email": 1, "role": "Sales Master Manager",
"export": 0, "share": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Master Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC",
"track_seen": 0 "states": [],
"translated_doctype": 1
} }

View File

@ -85,11 +85,15 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
} }
if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) { if (doc.docstatus == 1 && !["Lost", "Ordered"].includes(doc.status)) {
this.frm.add_custom_button( if (frappe.boot.sysdefaults.allow_sales_order_creation_for_expired_quotation
__("Sales Order"), || (!doc.valid_till)
() => this.make_sales_order(), || frappe.datetime.get_diff(doc.valid_till, frappe.datetime.get_today()) >= 0) {
__("Create") this.frm.add_custom_button(
); __("Sales Order"),
() => this.make_sales_order(),
__("Create")
);
}
if(doc.status!=="Ordered") { if(doc.status!=="Ordered") {
this.frm.add_custom_button(__('Set as Lost'), () => { this.frm.add_custom_button(__('Set as Lost'), () => {

View File

@ -249,6 +249,17 @@ def get_list_context(context=None):
@frappe.whitelist() @frappe.whitelist()
def make_sales_order(source_name: str, target_doc=None): def make_sales_order(source_name: str, target_doc=None):
if not frappe.db.get_singles_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
):
quotation = frappe.db.get_value(
"Quotation", source_name, ["transaction_date", "valid_till"], as_dict=1
)
if quotation.valid_till and (
quotation.valid_till < quotation.transaction_date or quotation.valid_till < getdate(nowdate())
):
frappe.throw(_("Validity period of this quotation has ended."))
return _make_sales_order(source_name, target_doc) return _make_sales_order(source_name, target_doc)

View File

@ -144,11 +144,21 @@ class TestQuotation(FrappeTestCase):
def test_so_from_expired_quotation(self): def test_so_from_expired_quotation(self):
from erpnext.selling.doctype.quotation.quotation import make_sales_order from erpnext.selling.doctype.quotation.quotation import make_sales_order
frappe.db.set_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 0
)
quotation = frappe.copy_doc(test_records[0]) quotation = frappe.copy_doc(test_records[0])
quotation.valid_till = add_days(nowdate(), -1) quotation.valid_till = add_days(nowdate(), -1)
quotation.insert() quotation.insert()
quotation.submit() quotation.submit()
self.assertRaises(frappe.ValidationError, make_sales_order, quotation.name)
frappe.db.set_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation", 1
)
make_sales_order(quotation.name) make_sales_order(quotation.name)
def test_shopping_cart_without_website_item(self): def test_shopping_cart_without_website_item(self):

View File

@ -1,94 +1,47 @@
{ {
"allow_copy": 0, "actions": [],
"allow_guest_to_view": 0, "autoname": "field:sales_partner_type",
"allow_import": 0, "creation": "2018-06-11 13:15:57.404716",
"allow_rename": 0, "doctype": "DocType",
"autoname": "field:sales_partner_type", "editable_grid": 1,
"beta": 0, "engine": "InnoDB",
"creation": "2018-06-11 13:15:57.404716", "field_order": [
"custom": 0, "sales_partner_type"
"docstatus": 0, ],
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [ "fields": [
{ {
"allow_bulk_edit": 0, "fieldname": "sales_partner_type",
"allow_in_quick_entry": 0, "fieldtype": "Data",
"allow_on_submit": 0, "in_list_view": 1,
"bold": 0, "label": "Sales Partner Type",
"collapsible": 0, "reqd": 1,
"columns": 0, "unique": 1
"fieldname": "sales_partner_type",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Sales Partner Type",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
} }
], ],
"has_web_view": 0, "links": [],
"hide_heading": 0, "modified": "2023-02-10 01:00:20.110800",
"hide_toolbar": 0, "modified_by": "Administrator",
"idx": 0, "module": "Selling",
"image_view": 0, "name": "Sales Partner Type",
"in_create": 0, "naming_rule": "By fieldname",
"is_submittable": 0, "owner": "Administrator",
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-06-11 13:45:13.554307",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Partner Type",
"name_case": "",
"owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0, "create": 1,
"cancel": 0, "delete": 1,
"create": 1, "email": 1,
"delete": 1, "export": 1,
"email": 1, "print": 1,
"export": 1, "read": 1,
"if_owner": 0, "report": 1,
"import": 0, "role": "System Manager",
"permlevel": 0, "share": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1 "write": 1
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0, "sort_field": "modified",
"read_only_onload": 0, "sort_order": "DESC",
"show_name_in_global_search": 0, "states": [],
"sort_field": "modified", "translated_doctype": 1
"sort_order": "DESC",
"track_changes": 0,
"track_seen": 0
} }

View File

@ -27,6 +27,7 @@
"column_break_5", "column_break_5",
"allow_multiple_items", "allow_multiple_items",
"allow_against_multiple_purchase_orders", "allow_against_multiple_purchase_orders",
"allow_sales_order_creation_for_expired_quotation",
"hide_tax_id", "hide_tax_id",
"enable_discount_accounting" "enable_discount_accounting"
], ],
@ -172,6 +173,12 @@
"fieldname": "enable_discount_accounting", "fieldname": "enable_discount_accounting",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Enable Discount Accounting for Selling" "label": "Enable Discount Accounting for Selling"
},
{
"default": "0",
"fieldname": "allow_sales_order_creation_for_expired_quotation",
"fieldtype": "Check",
"label": "Allow Sales Order Creation For Expired Quotation"
} }
], ],
"icon": "fa fa-cog", "icon": "fa fa-cog",
@ -179,7 +186,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2022-05-31 19:39:48.398738", "modified": "2023-02-04 12:37:53.380857",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Selling Settings", "name": "Selling Settings",

View File

@ -322,6 +322,11 @@ erpnext.PointOfSale.Payment = class {
this.focus_on_default_mop(); this.focus_on_default_mop();
} }
after_render() {
const frm = this.events.get_frm();
frm.script_manager.trigger("after_payment_render", frm.doc.doctype, frm.doc.docname);
}
edit_cart() { edit_cart() {
this.events.toggle_other_sections(false); this.events.toggle_other_sections(false);
this.toggle_component(false); this.toggle_component(false);
@ -332,6 +337,7 @@ erpnext.PointOfSale.Payment = class {
this.toggle_component(true); this.toggle_component(true);
this.render_payment_section(); this.render_payment_section();
this.after_render();
} }
toggle_remarks_control() { toggle_remarks_control() {

View File

@ -103,6 +103,11 @@ function get_filters() {
return options return options
} }
}, },
{
"fieldname":"only_immediate_upcoming_term",
"label": __("Show only the Immediate Upcoming Term"),
"fieldtype": "Check",
},
] ]
return filters; return filters;
} }

View File

@ -4,6 +4,7 @@
import frappe import frappe
from frappe import _, qb, query_builder from frappe import _, qb, query_builder
from frappe.query_builder import Criterion, functions from frappe.query_builder import Criterion, functions
from frappe.utils.dateutils import getdate
def get_columns(): def get_columns():
@ -208,6 +209,7 @@ def get_so_with_invoices(filters):
) )
.where( .where(
(so.docstatus == 1) (so.docstatus == 1)
& (so.status.isin(["To Deliver and Bill", "To Bill"]))
& (so.payment_terms_template != "NULL") & (so.payment_terms_template != "NULL")
& (so.company == conditions.company) & (so.company == conditions.company)
& (so.transaction_date[conditions.start_date : conditions.end_date]) & (so.transaction_date[conditions.start_date : conditions.end_date])
@ -291,6 +293,18 @@ def filter_on_calculated_status(filters, sales_orders):
return sales_orders return sales_orders
def filter_for_immediate_upcoming_term(filters, sales_orders):
if filters.only_immediate_upcoming_term and sales_orders:
immediate_term_found = set()
filtered_data = []
for order in sales_orders:
if order.name not in immediate_term_found and order.due_date > getdate():
filtered_data.append(order)
immediate_term_found.add(order.name)
return filtered_data
return sales_orders
def execute(filters=None): def execute(filters=None):
columns = get_columns() columns = get_columns()
sales_orders, so_invoices = get_so_with_invoices(filters) sales_orders, so_invoices = get_so_with_invoices(filters)
@ -298,6 +312,8 @@ def execute(filters=None):
sales_orders = filter_on_calculated_status(filters, sales_orders) sales_orders = filter_on_calculated_status(filters, sales_orders)
sales_orders = filter_for_immediate_upcoming_term(filters, sales_orders)
prepare_chart(sales_orders) prepare_chart(sales_orders)
data = sales_orders data = sales_orders

View File

@ -175,7 +175,9 @@ def prepare_data(data, so_elapsed_time, filters):
# update existing entry # update existing entry
so_row = sales_order_map[so_name] so_row = sales_order_map[so_name]
so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"])) so_row["required_date"] = max(getdate(so_row["delivery_date"]), getdate(row["delivery_date"]))
so_row["delay"] = min(so_row["delay"], row["delay"]) so_row["delay"] = (
min(so_row["delay"], row["delay"]) if row["delay"] and so_row["delay"] else so_row["delay"]
)
# sum numeric columns # sum numeric columns
fields = [ fields = [

View File

@ -31,7 +31,7 @@
"icon": "fa fa-bookmark", "icon": "fa fa-bookmark",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2022-06-28 17:10:26.853753", "modified": "2023-02-10 01:53:41.319386",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Designation", "name": "Designation",
@ -58,5 +58,6 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [] "states": [],
"translated_doctype": 1
} }

View File

@ -1,13 +1,6 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt // License: GNU General Public License v3. See license.txt
// frappe.ui.form.on("Terms and Conditions", {
// refresh(frm) {}
//--------- ONLOAD ------------- // });
cur_frm.cscript.onload = function(doc, cdt, cdn) {
}
cur_frm.cscript.refresh = function(doc, cdt, cdn) {
}

View File

@ -33,7 +33,6 @@
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Disabled" "label": "Disabled"
}, },
{ {
@ -60,12 +59,14 @@
"default": "1", "default": "1",
"fieldname": "selling", "fieldname": "selling",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Selling" "label": "Selling"
}, },
{ {
"default": "1", "default": "1",
"fieldname": "buying", "fieldname": "buying",
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1,
"label": "Buying" "label": "Buying"
}, },
{ {
@ -76,10 +77,11 @@
"icon": "icon-legal", "icon": "icon-legal",
"idx": 1, "idx": 1,
"links": [], "links": [],
"modified": "2022-06-16 15:07:38.094844", "modified": "2023-02-01 14:33:39.246532",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Terms and Conditions", "name": "Terms and Conditions",
"naming_rule": "By fieldname",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -133,5 +135,6 @@
"quick_entry": 1, "quick_entry": 1,
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC" "sort_order": "ASC",
"states": []
} }

View File

@ -0,0 +1,31 @@
Accountant
Administrative Assistant
Administrative Officer
Analyst
Associate
Business Analyst
Business Development Manager
Consultant
Chief Executive Officer
Chief Financial Officer
Chief Operating Officer
Chief Technology Officer
Customer Service Representative
Designer
Engineer
Executive Assistant
Finance Manager
HR Manager
Head of Marketing and Sales
Manager
Managing Director
Marketing Manager
Marketing Specialist
President
Product Manager
Project Manager
Researcher
Sales Representative
Secretary
Software Developer
Vice President

View File

@ -1,57 +0,0 @@
from frappe import _
def get_industry_types():
return [
_("Accounting"),
_("Advertising"),
_("Aerospace"),
_("Agriculture"),
_("Airline"),
_("Apparel & Accessories"),
_("Automotive"),
_("Banking"),
_("Biotechnology"),
_("Broadcasting"),
_("Brokerage"),
_("Chemical"),
_("Computer"),
_("Consulting"),
_("Consumer Products"),
_("Cosmetics"),
_("Defense"),
_("Department Stores"),
_("Education"),
_("Electronics"),
_("Energy"),
_("Entertainment & Leisure"),
_("Executive Search"),
_("Financial Services"),
_("Food, Beverage & Tobacco"),
_("Grocery"),
_("Health Care"),
_("Internet Publishing"),
_("Investment Banking"),
_("Legal"),
_("Manufacturing"),
_("Motion Picture & Video"),
_("Music"),
_("Newspaper Publishers"),
_("Online Auctions"),
_("Pension Funds"),
_("Pharmaceuticals"),
_("Private Equity"),
_("Publishing"),
_("Real Estate"),
_("Retail & Wholesale"),
_("Securities & Commodity Exchanges"),
_("Service"),
_("Soap & Detergent"),
_("Software"),
_("Sports"),
_("Technology"),
_("Telecommunications"),
_("Television"),
_("Transportation"),
_("Venture Capital"),
]

View File

@ -0,0 +1,51 @@
Accounting
Advertising
Aerospace
Agriculture
Airline
Apparel & Accessories
Automotive
Banking
Biotechnology
Broadcasting
Brokerage
Chemical
Computer
Consulting
Consumer Products
Cosmetics
Defense
Department Stores
Education
Electronics
Energy
Entertainment & Leisure
Executive Search
Financial Services
Food, Beverage & Tobacco
Grocery
Health Care
Internet Publishing
Investment Banking
Legal
Manufacturing
Motion Picture & Video
Music
Newspaper Publishers
Online Auctions
Pension Funds
Pharmaceuticals
Private Equity
Publishing
Real Estate
Retail & Wholesale
Securities & Commodity Exchanges
Service
Soap & Detergent
Software
Sports
Technology
Telecommunications
Television
Transportation
Venture Capital

View File

@ -0,0 +1,10 @@
Existing Customer
Reference
Advertisement
Cold Calling
Exhibition
Supplier Reference
Mass Mailing
Customer's Vendor
Campaign
Walk In

View File

@ -0,0 +1,7 @@
Channel Partner
Distributor
Dealer
Agent
Retailer
Implementation Partner
Reseller

View File

@ -0,0 +1,8 @@
Prospecting
Qualification
Needs Analysis
Value Proposition
Identifying Decision Makers
Perception Analysis
Proposal/Price Quote
Negotiation/Review

View File

@ -4,6 +4,7 @@
import json import json
import os import os
from pathlib import Path
import frappe import frappe
from frappe import _ from frappe import _
@ -16,28 +17,10 @@ from frappe.utils import cstr, getdate
from erpnext.accounts.doctype.account.account import RootNotEditable from erpnext.accounts.doctype.account.account import RootNotEditable
from erpnext.regional.address_template.setup import set_up_address_templates from erpnext.regional.address_template.setup import set_up_address_templates
default_lead_sources = [
"Existing Customer",
"Reference",
"Advertisement",
"Cold Calling",
"Exhibition",
"Supplier Reference",
"Mass Mailing",
"Customer's Vendor",
"Campaign",
"Walk In",
]
default_sales_partner_type = [ def read_lines(filename: str) -> list[str]:
"Channel Partner", """Return a list of lines from a file in the data directory."""
"Distributor", return (Path(__file__).parent.parent / "data" / filename).read_text().splitlines()
"Dealer",
"Agent",
"Retailer",
"Implementation Partner",
"Reseller",
]
def install(country=None): def install(country=None):
@ -85,7 +68,11 @@ def install(country=None):
# Stock Entry Type # Stock Entry Type
{"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"}, {"doctype": "Stock Entry Type", "name": "Material Issue", "purpose": "Material Issue"},
{"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"}, {"doctype": "Stock Entry Type", "name": "Material Receipt", "purpose": "Material Receipt"},
{"doctype": "Stock Entry Type", "name": "Material Transfer", "purpose": "Material Transfer"}, {
"doctype": "Stock Entry Type",
"name": "Material Transfer",
"purpose": "Material Transfer",
},
{"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"}, {"doctype": "Stock Entry Type", "name": "Manufacture", "purpose": "Manufacture"},
{"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"}, {"doctype": "Stock Entry Type", "name": "Repack", "purpose": "Repack"},
{ {
@ -103,22 +90,6 @@ def install(country=None):
"name": "Material Consumption for Manufacture", "name": "Material Consumption for Manufacture",
"purpose": "Material Consumption for Manufacture", "purpose": "Material Consumption for Manufacture",
}, },
# Designation
{"doctype": "Designation", "designation_name": _("CEO")},
{"doctype": "Designation", "designation_name": _("Manager")},
{"doctype": "Designation", "designation_name": _("Analyst")},
{"doctype": "Designation", "designation_name": _("Engineer")},
{"doctype": "Designation", "designation_name": _("Accountant")},
{"doctype": "Designation", "designation_name": _("Secretary")},
{"doctype": "Designation", "designation_name": _("Associate")},
{"doctype": "Designation", "designation_name": _("Administrative Officer")},
{"doctype": "Designation", "designation_name": _("Business Development Manager")},
{"doctype": "Designation", "designation_name": _("HR Manager")},
{"doctype": "Designation", "designation_name": _("Project Manager")},
{"doctype": "Designation", "designation_name": _("Head of Marketing and Sales")},
{"doctype": "Designation", "designation_name": _("Software Developer")},
{"doctype": "Designation", "designation_name": _("Designer")},
{"doctype": "Designation", "designation_name": _("Researcher")},
# territory: with two default territories, one for home country and one named Rest of the World # territory: with two default territories, one for home country and one named Rest of the World
{ {
"doctype": "Territory", "doctype": "Territory",
@ -291,28 +262,18 @@ def install(country=None):
{"doctype": "Market Segment", "market_segment": _("Lower Income")}, {"doctype": "Market Segment", "market_segment": _("Lower Income")},
{"doctype": "Market Segment", "market_segment": _("Middle Income")}, {"doctype": "Market Segment", "market_segment": _("Middle Income")},
{"doctype": "Market Segment", "market_segment": _("Upper Income")}, {"doctype": "Market Segment", "market_segment": _("Upper Income")},
# Sales Stages
{"doctype": "Sales Stage", "stage_name": _("Prospecting")},
{"doctype": "Sales Stage", "stage_name": _("Qualification")},
{"doctype": "Sales Stage", "stage_name": _("Needs Analysis")},
{"doctype": "Sales Stage", "stage_name": _("Value Proposition")},
{"doctype": "Sales Stage", "stage_name": _("Identifying Decision Makers")},
{"doctype": "Sales Stage", "stage_name": _("Perception Analysis")},
{"doctype": "Sales Stage", "stage_name": _("Proposal/Price Quote")},
{"doctype": "Sales Stage", "stage_name": _("Negotiation/Review")},
# Warehouse Type # Warehouse Type
{"doctype": "Warehouse Type", "name": "Transit"}, {"doctype": "Warehouse Type", "name": "Transit"},
] ]
from erpnext.setup.setup_wizard.data.industry_type import get_industry_types for doctype, title_field, filename in (
("Designation", "designation_name", "designation.txt"),
records += [{"doctype": "Industry Type", "industry": d} for d in get_industry_types()] ("Sales Stage", "stage_name", "sales_stage.txt"),
# records += [{"doctype":"Operation", "operation": d} for d in get_operations()] ("Industry Type", "industry", "industry_type.txt"),
records += [{"doctype": "Lead Source", "source_name": _(d)} for d in default_lead_sources] ("Lead Source", "source_name", "lead_source.txt"),
("Sales Partner Type", "sales_partner_type", "sales_partner_type.txt"),
records += [ ):
{"doctype": "Sales Partner Type", "sales_partner_type": _(d)} for d in default_sales_partner_type records += [{"doctype": doctype, title_field: title} for title in read_lines(filename)]
]
base_path = frappe.get_app_path("erpnext", "stock", "doctype") base_path = frappe.get_app_path("erpnext", "stock", "doctype")
response = frappe.read_file( response = frappe.read_file(
@ -335,16 +296,11 @@ def install(country=None):
make_default_records() make_default_records()
make_records(records) make_records(records)
set_up_address_templates(default_country=country) set_up_address_templates(default_country=country)
set_more_defaults()
update_global_search_doctypes()
def set_more_defaults():
# Do more setup stuff that can be done here with no dependencies
update_selling_defaults() update_selling_defaults()
update_buying_defaults() update_buying_defaults()
add_uom_data() add_uom_data()
update_item_variant_settings() update_item_variant_settings()
update_global_search_doctypes()
def update_selling_defaults(): def update_selling_defaults():
@ -381,7 +337,7 @@ def add_uom_data():
) )
for d in uoms: for d in uoms:
if not frappe.db.exists("UOM", _(d.get("uom_name"))): if not frappe.db.exists("UOM", _(d.get("uom_name"))):
uom_doc = frappe.get_doc( frappe.get_doc(
{ {
"doctype": "UOM", "doctype": "UOM",
"uom_name": _(d.get("uom_name")), "uom_name": _(d.get("uom_name")),
@ -402,9 +358,10 @@ def add_uom_data():
frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert() frappe.get_doc({"doctype": "UOM Category", "category_name": _(d.get("category"))}).db_insert()
if not frappe.db.exists( if not frappe.db.exists(
"UOM Conversion Factor", {"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))} "UOM Conversion Factor",
{"from_uom": _(d.get("from_uom")), "to_uom": _(d.get("to_uom"))},
): ):
uom_conversion = frappe.get_doc( frappe.get_doc(
{ {
"doctype": "UOM Conversion Factor", "doctype": "UOM Conversion Factor",
"category": _(d.get("category")), "category": _(d.get("category")),
@ -412,7 +369,7 @@ def add_uom_data():
"to_uom": _(d.get("to_uom")), "to_uom": _(d.get("to_uom")),
"value": d.get("value"), "value": d.get("value"),
} }
).insert(ignore_permissions=True) ).db_insert()
def add_market_segments(): def add_market_segments():
@ -468,7 +425,7 @@ def install_company(args):
make_records(records) make_records(records)
def install_defaults(args=None): def install_defaults(args=None): # nosemgrep
records = [ records = [
# Price Lists # Price Lists
{ {
@ -493,7 +450,7 @@ def install_defaults(args=None):
# enable default currency # enable default currency
frappe.db.set_value("Currency", args.get("currency"), "enabled", 1) frappe.db.set_value("Currency", args.get("currency"), "enabled", 1)
frappe.db.set_value("Stock Settings", None, "email_footer_address", args.get("company_name")) frappe.db.set_single_value("Stock Settings", "email_footer_address", args.get("company_name"))
set_global_defaults(args) set_global_defaults(args)
update_stock_settings() update_stock_settings()
@ -540,7 +497,8 @@ def create_bank_account(args):
company_name = args.get("company_name") company_name = args.get("company_name")
bank_account_group = frappe.db.get_value( bank_account_group = frappe.db.get_value(
"Account", {"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name} "Account",
{"account_type": "Bank", "is_group": 1, "root_type": "Asset", "company": company_name},
) )
if bank_account_group: if bank_account_group:
bank_account = frappe.get_doc( bank_account = frappe.get_doc(

View File

@ -158,6 +158,7 @@ def make_taxes_and_charges_template(company_name, doctype, template):
# Ingone validations to make doctypes faster # Ingone validations to make doctypes faster
doc.flags.ignore_links = True doc.flags.ignore_links = True
doc.flags.ignore_validate = True doc.flags.ignore_validate = True
doc.flags.ignore_mandatory = True
doc.insert(ignore_permissions=True) doc.insert(ignore_permissions=True)
return doc return doc

View File

@ -25,6 +25,12 @@ def boot_session(bootinfo):
frappe.db.get_single_value("CRM Settings", "default_valid_till") frappe.db.get_single_value("CRM Settings", "default_valid_till")
) )
bootinfo.sysdefaults.allow_sales_order_creation_for_expired_quotation = cint(
frappe.db.get_single_value(
"Selling Settings", "allow_sales_order_creation_for_expired_quotation"
)
)
# if no company, show a dialog box to create a new company # if no company, show a dialog box to create a new company
bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0] bootinfo.customer_count = frappe.db.sql("""SELECT count(*) FROM `tabCustomer`""")[0][0]

View File

@ -38,5 +38,19 @@
"price_list_rate": 1000, "price_list_rate": 1000,
"valid_from": "2017-04-10", "valid_from": "2017-04-10",
"valid_upto": "2017-04-17" "valid_upto": "2017-04-17"
},
{
"doctype": "Item Price",
"item_code": "_Test Item",
"price_list": "_Test Buying Price List",
"price_list_rate": 100,
"supplier": "_Test Supplier"
},
{
"doctype": "Item Price",
"item_code": "_Test Item",
"price_list": "_Test Selling Price List",
"price_list_rate": 200,
"customer": "_Test Customer"
} }
] ]

View File

@ -110,8 +110,11 @@ frappe.ui.form.on('Material Request', {
if (frm.doc.material_request_type === "Material Transfer") { if (frm.doc.material_request_type === "Material Transfer") {
add_create_pick_list_button(); add_create_pick_list_button();
frm.add_custom_button(__("Transfer Material"), frm.add_custom_button(__("Material Transfer"),
() => frm.events.make_stock_entry(frm), __('Create')); () => frm.events.make_stock_entry(frm), __('Create'));
frm.add_custom_button(__("Material Transfer (In Transit)"),
() => frm.events.make_in_transit_stock_entry(frm), __('Create'));
} }
if (frm.doc.material_request_type === "Material Issue") { if (frm.doc.material_request_type === "Material Issue") {
@ -333,6 +336,46 @@ frappe.ui.form.on('Material Request', {
}); });
}, },
make_in_transit_stock_entry(frm) {
frappe.prompt(
[
{
label: __('In Transit Warehouse'),
fieldname: 'in_transit_warehouse',
fieldtype: 'Link',
options: 'Warehouse',
reqd: 1,
get_query: () => {
return{
filters: {
'company': frm.doc.company,
'is_group': 0,
'warehouse_type': 'Transit'
}
}
}
}
],
(values) => {
frappe.call({
method: "erpnext.stock.doctype.material_request.material_request.make_in_transit_stock_entry",
args: {
source_name: frm.doc.name,
in_transit_warehouse: values.in_transit_warehouse
},
callback: function(r) {
if (r.message) {
let doc = frappe.model.sync(r.message);
frappe.set_route('Form', doc[0].doctype, doc[0].name);
}
}
})
},
__('In Transit Transfer'),
__("Create Stock Entry")
)
},
create_pick_list: (frm) => { create_pick_list: (frm) => {
frappe.model.open_mapped_doc({ frappe.model.open_mapped_doc({
method: "erpnext.stock.doctype.material_request.material_request.create_pick_list", method: "erpnext.stock.doctype.material_request.material_request.create_pick_list",

View File

@ -716,3 +716,14 @@ def create_pick_list(source_name, target_doc=None):
doc.set_item_locations() doc.set_item_locations()
return doc return doc
@frappe.whitelist()
def make_in_transit_stock_entry(source_name, in_transit_warehouse):
ste_doc = make_stock_entry(source_name)
ste_doc.add_to_transit = 1
for row in ste_doc.items:
row.t_warehouse = in_transit_warehouse
return ste_doc

View File

@ -11,6 +11,7 @@ from frappe.utils import flt, today
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.material_request.material_request import ( from erpnext.stock.doctype.material_request.material_request import (
make_in_transit_stock_entry,
make_purchase_order, make_purchase_order,
make_stock_entry, make_stock_entry,
make_supplier_quotation, make_supplier_quotation,
@ -56,6 +57,22 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(se.doctype, "Stock Entry") self.assertEqual(se.doctype, "Stock Entry")
self.assertEqual(len(se.get("items")), len(mr.get("items"))) self.assertEqual(len(se.get("items")), len(mr.get("items")))
def test_in_transit_make_stock_entry(self):
mr = frappe.copy_doc(test_records[0]).insert()
self.assertRaises(frappe.ValidationError, make_stock_entry, mr.name)
mr = frappe.get_doc("Material Request", mr.name)
mr.material_request_type = "Material Transfer"
mr.submit()
in_transit_warehouse = get_in_transit_warehouse(mr.company)
se = make_in_transit_stock_entry(mr.name, in_transit_warehouse)
self.assertEqual(se.doctype, "Stock Entry")
for row in se.get("items"):
self.assertEqual(row.t_warehouse, in_transit_warehouse)
def _insert_stock_entry(self, qty1, qty2, warehouse=None): def _insert_stock_entry(self, qty1, qty2, warehouse=None):
se = frappe.get_doc( se = frappe.get_doc(
{ {
@ -742,6 +759,36 @@ class TestMaterialRequest(FrappeTestCase):
self.assertEqual(existing_requested_qty, current_requested_qty) self.assertEqual(existing_requested_qty, current_requested_qty)
def get_in_transit_warehouse(company):
if not frappe.db.exists("Warehouse Type", "Transit"):
frappe.get_doc(
{
"doctype": "Warehouse Type",
"name": "Transit",
}
).insert()
in_transit_warehouse = frappe.db.exists(
"Warehouse", {"warehouse_type": "Transit", "company": company}
)
if not in_transit_warehouse:
in_transit_warehouse = (
frappe.get_doc(
{
"doctype": "Warehouse",
"warehouse_name": "Transit",
"warehouse_type": "Transit",
"company": company,
}
)
.insert()
.name
)
return in_transit_warehouse
def make_material_request(**args): def make_material_request(**args):
args = frappe._dict(args) args = frappe._dict(args)
mr = frappe.new_doc("Material Request") mr = frappe.new_doc("Material Request")

View File

@ -31,5 +31,21 @@
"enabled": 1, "enabled": 1,
"price_list_name": "_Test Price List Rest of the World", "price_list_name": "_Test Price List Rest of the World",
"selling": 1 "selling": 1
},
{
"buying": 0,
"currency": "USD",
"doctype": "Price List",
"enabled": 1,
"price_list_name": "_Test Selling Price List",
"selling": 1
},
{
"buying": 1,
"currency": "USD",
"doctype": "Price List",
"enabled": 1,
"price_list_name": "_Test Buying Price List",
"selling": 0
} }
] ]

View File

@ -2494,7 +2494,7 @@ def get_uom_details(item_code, uom, qty):
if not conversion_factor: if not conversion_factor:
frappe.msgprint( frappe.msgprint(
_("UOM coversion factor required for UOM: {0} in Item: {1}").format(uom, item_code) _("UOM conversion factor required for UOM: {0} in Item: {1}").format(uom, item_code)
) )
ret = {"uom": ""} ret = {"uom": ""}
else: else:

View File

@ -88,8 +88,15 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
update_party_blanket_order(args, out) update_party_blanket_order(args, out)
# Never try to find a customer price if customer is set in these Doctype
current_customer = args.customer
if args.get("doctype") in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
args.customer = None
out.update(get_price_list_rate(args, item)) out.update(get_price_list_rate(args, item))
args.customer = current_customer
if args.customer and cint(args.is_pos): if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args, update_data=True)) out.update(get_pos_profile_item_details(args.company, args, update_data=True))

View File

@ -121,7 +121,7 @@ def get_reserved_qty(item_code, warehouse):
and parenttype='Sales Order' and parenttype='Sales Order'
and item_code != parent_item and item_code != parent_item
and exists (select * from `tabSales Order` so and exists (select * from `tabSales Order` so
where name = dnpi_in.parent and docstatus = 1 and status != 'Closed') where name = dnpi_in.parent and docstatus = 1 and status not in ('On Hold', 'Closed'))
) dnpi) ) dnpi)
union union
(select stock_qty as dnpi_qty, qty as so_item_qty, (select stock_qty as dnpi_qty, qty as so_item_qty,
@ -131,7 +131,7 @@ def get_reserved_qty(item_code, warehouse):
and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0) and (so_item.delivered_by_supplier is null or so_item.delivered_by_supplier = 0)
and exists(select * from `tabSales Order` so and exists(select * from `tabSales Order` so
where so.name = so_item.parent and so.docstatus = 1 where so.name = so_item.parent and so.docstatus = 1
and so.status != 'Closed')) and so.status not in ('On Hold', 'Closed')))
) tab ) tab
where where
so_item_qty >= so_item_delivered_qty so_item_qty >= so_item_delivered_qty

View File

@ -0,0 +1,40 @@
import json
import frappe
from frappe.test_runner import make_test_records
from frappe.tests.utils import FrappeTestCase
from erpnext.stock.get_item_details import get_item_details
test_ignore = ["BOM"]
test_dependencies = ["Customer", "Supplier", "Item", "Price List", "Item Price"]
class TestGetItemDetail(FrappeTestCase):
def setUp(self):
make_test_records("Price List")
super().setUp()
def test_get_item_detail_purchase_order(self):
args = frappe._dict(
{
"item_code": "_Test Item",
"company": "_Test Company",
"customer": "_Test Customer",
"conversion_rate": 1.0,
"price_list_currency": "USD",
"plc_conversion_rate": 1.0,
"doctype": "Purchase Order",
"name": None,
"supplier": "_Test Supplier",
"transaction_date": None,
"conversion_rate": 1.0,
"price_list": "_Test Buying Price List",
"is_subcontracted": 0,
"ignore_pricing_rule": 1,
"qty": 1,
}
)
details = get_item_details(args)
self.assertEqual(details.get("price_list_rate"), 100)

View File

@ -51,21 +51,31 @@ def get_tabs(categories):
return tab_values return tab_values
def get_category_records(categories): def get_category_records(categories: list):
categorical_data = {} categorical_data = {}
for category in categories:
if category == "item_group": for c in categories:
if c == "item_group":
categorical_data["item_group"] = frappe.db.get_all( categorical_data["item_group"] = frappe.db.get_all(
"Item Group", "Item Group",
filters={"parent_item_group": "All Item Groups", "show_in_website": 1}, filters={"parent_item_group": "All Item Groups", "show_in_website": 1},
fields=["name", "parent_item_group", "is_group", "image", "route"], fields=["name", "parent_item_group", "is_group", "image", "route"],
) )
else:
doctype = frappe.unscrub(category) continue
fields = ["name"]
if frappe.get_meta(doctype, cached=True).get_field("image"): doctype = frappe.unscrub(c)
fields = ["name"]
try:
meta = frappe.get_meta(doctype, cached=True)
if meta.get_field("image"):
fields += ["image"] fields += ["image"]
categorical_data[category] = frappe.db.get_all(doctype, fields=fields) data = frappe.db.get_all(doctype, fields=fields)
categorical_data[c] = data
except BaseException:
frappe.throw(_("DocType {} not found").format(doctype))
continue
return categorical_data return categorical_data