Merge branch 'develop' into fixed-process-loss-in-job-card

This commit is contained in:
rohitwaghchaure 2023-06-12 19:04:19 +05:30 committed by GitHub
commit 93fe923e2a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
58 changed files with 1236 additions and 1530 deletions

View File

@ -50,13 +50,15 @@ class AccountingDimension(Document):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long") frappe.enqueue(
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
def on_trash(self): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self)
else: else:
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long") frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True)
def set_fieldname_and_label(self): def set_fieldname_and_label(self):
if not self.label: if not self.label:

View File

@ -41,7 +41,7 @@ frappe.ui.form.on("Bank Clearance", {
frm.trigger("get_payment_entries") frm.trigger("get_payment_entries")
); );
frm.change_custom_button_type('Get Payment Entries', null, 'primary'); frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary');
}, },
update_clearance_date: function(frm) { update_clearance_date: function(frm) {
@ -53,8 +53,8 @@ frappe.ui.form.on("Bank Clearance", {
frm.refresh_fields(); frm.refresh_fields();
if (!frm.doc.payment_entries.length) { if (!frm.doc.payment_entries.length) {
frm.change_custom_button_type('Get Payment Entries', null, 'primary'); frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary');
frm.change_custom_button_type('Update Clearance Date', null, 'default'); frm.change_custom_button_type(__('Update Clearance Date'), null, 'default');
} }
} }
}); });
@ -72,8 +72,8 @@ frappe.ui.form.on("Bank Clearance", {
frm.trigger("update_clearance_date") frm.trigger("update_clearance_date")
); );
frm.change_custom_button_type('Get Payment Entries', null, 'default'); frm.change_custom_button_type(__('Get Payment Entries'), null, 'default');
frm.change_custom_button_type('Update Clearance Date', null, 'primary'); frm.change_custom_button_type(__('Update Clearance Date'), null, 'primary');
} }
} }
}); });

View File

@ -81,7 +81,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.add_custom_button(__('Get Unreconciled Entries'), function() { frm.add_custom_button(__('Get Unreconciled Entries'), function() {
frm.trigger("make_reconciliation_tool"); frm.trigger("make_reconciliation_tool");
}); });
frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary'); frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
}, },

View File

@ -245,6 +245,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -315,10 +316,11 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-08-03 18:55:43.683053", "modified": "2023-06-03 16:24:01.677026",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning", "name": "Dunning",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -365,6 +367,7 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "customer_name", "title_field": "customer_name",
"track_changes": 1 "track_changes": 1
} }

View File

@ -952,6 +952,7 @@ class JournalEntry(AccountsController):
blank_row.debit_in_account_currency = abs(diff) blank_row.debit_in_account_currency = abs(diff)
blank_row.debit = abs(diff) blank_row.debit = abs(diff)
self.set_total_debit_credit()
self.validate_total_debit_and_credit() self.validate_total_debit_and_credit()
@frappe.whitelist() @frappe.whitelist()

View File

@ -65,22 +65,22 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.frm.add_custom_button(__('Get Unreconciled Entries'), () => this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
this.frm.trigger("get_unreconciled_entries") this.frm.trigger("get_unreconciled_entries")
); );
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary'); this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
} }
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) { if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
this.frm.add_custom_button(__('Allocate'), () => this.frm.add_custom_button(__('Allocate'), () =>
this.frm.trigger("allocate") this.frm.trigger("allocate")
); );
this.frm.change_custom_button_type('Allocate', null, 'primary'); this.frm.change_custom_button_type(__('Allocate'), null, 'primary');
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'default');
} }
if (this.frm.doc.allocation.length) { if (this.frm.doc.allocation.length) {
this.frm.add_custom_button(__('Reconcile'), () => this.frm.add_custom_button(__('Reconcile'), () =>
this.frm.trigger("reconcile") this.frm.trigger("reconcile")
); );
this.frm.change_custom_button_type('Reconcile', null, 'primary'); this.frm.change_custom_button_type(__('Reconcile'), null, 'primary');
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default'); this.frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'default');
this.frm.change_custom_button_type('Allocate', null, 'default'); this.frm.change_custom_button_type(__('Allocate'), null, 'default');
} }
// check for any running reconciliation jobs // check for any running reconciliation jobs

View File

@ -6,7 +6,6 @@ import frappe
from frappe import _, msgprint, qb from frappe import _, msgprint, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn from frappe.query_builder.custom import ConstantColumn
from frappe.query_builder.functions import IfNull
from frappe.utils import flt, get_link_to_form, getdate, nowdate, today from frappe.utils import flt, get_link_to_form, getdate, nowdate, today
import erpnext import erpnext
@ -127,12 +126,29 @@ class PaymentReconciliation(Document):
return list(journal_entries) return list(journal_entries)
def get_return_invoices(self):
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
doc = qb.DocType(voucher_type)
self.return_invoices = (
qb.from_(doc)
.select(
ConstantColumn(voucher_type).as_("voucher_type"),
doc.name.as_("voucher_no"),
doc.return_against,
)
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
)
.run(as_dict=True)
)
def get_dr_or_cr_notes(self): def get_dr_or_cr_notes(self):
self.build_qb_filter_conditions(get_return_invoices=True) self.build_qb_filter_conditions(get_return_invoices=True)
ple = qb.DocType("Payment Ledger Entry") ple = qb.DocType("Payment Ledger Entry")
voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
if erpnext.get_party_account_type(self.party_type) == "Receivable": if erpnext.get_party_account_type(self.party_type) == "Receivable":
self.common_filter_conditions.append(ple.account_type == "Receivable") self.common_filter_conditions.append(ple.account_type == "Receivable")
@ -140,19 +156,10 @@ class PaymentReconciliation(Document):
self.common_filter_conditions.append(ple.account_type == "Payable") self.common_filter_conditions.append(ple.account_type == "Payable")
self.common_filter_conditions.append(ple.account == self.receivable_payable_account) self.common_filter_conditions.append(ple.account == self.receivable_payable_account)
# get return invoices self.get_return_invoices()
doc = qb.DocType(voucher_type) return_invoices = [
return_invoices = ( x for x in self.return_invoices if x.return_against == None or x.return_against == ""
qb.from_(doc) ]
.select(ConstantColumn(voucher_type).as_("voucher_type"), doc.name.as_("voucher_no"))
.where(
(doc.docstatus == 1)
& (doc[frappe.scrub(self.party_type)] == self.party)
& (doc.is_return == 1)
& (IfNull(doc.return_against, "") == "")
)
.run(as_dict=True)
)
outstanding_dr_or_cr = [] outstanding_dr_or_cr = []
if return_invoices: if return_invoices:
@ -204,6 +211,9 @@ class PaymentReconciliation(Document):
accounting_dimensions=self.accounting_dimension_filter_conditions, accounting_dimensions=self.accounting_dimension_filter_conditions,
) )
cr_dr_notes = [x.voucher_no for x in self.return_invoices]
non_reconciled_invoices = [x for x in non_reconciled_invoices if x.voucher_no not in cr_dr_notes]
if self.invoice_limit: if self.invoice_limit:
non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit] non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]

View File

@ -44,6 +44,7 @@ class PeriodClosingVoucher(AccountsController):
voucher_type="Period Closing Voucher", voucher_type="Period Closing Voucher",
voucher_no=self.name, voucher_no=self.name,
queue="long", queue="long",
enqueue_after_commit=True,
) )
frappe.msgprint( frappe.msgprint(
_("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True _("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True

View File

@ -442,6 +442,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1554,11 +1555,10 @@
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-09-30 03:49:50.455199", "modified": "2023-06-03 16:23:41.083409",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "POS Invoice", "name": "POS Invoice",
"name_case": "Title Case",
"naming_rule": "By \"Naming Series\" field", "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [

View File

@ -158,7 +158,7 @@ def get_customers_based_on_territory_or_customer_group(customer_collection, coll
return frappe.get_list( return frappe.get_list(
"Customer", "Customer",
fields=["name", "customer_name", "email_id"], fields=["name", "customer_name", "email_id"],
filters=[[fields_dict[customer_collection], "IN", selected]], filters=[["disabled", "=", 0], [fields_dict[customer_collection], "IN", selected]],
) )

View File

@ -443,12 +443,14 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -1364,12 +1366,12 @@
"depends_on": "eval:doc.update_stock && doc.is_internal_supplier", "depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
"fieldname": "set_from_warehouse", "fieldname": "set_from_warehouse",
"fieldtype": "Link", "fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Set From Warehouse", "label": "Set From Warehouse",
"no_copy": 1, "no_copy": 1,
"options": "Warehouse", "options": "Warehouse",
"print_hide": 1, "print_hide": 1,
"print_width": "50px", "print_width": "50px",
"ignore_user_permissions": 1,
"width": "50px" "width": "50px"
}, },
{ {
@ -1573,7 +1575,7 @@
"idx": 204, "idx": 204,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-29 12:57:50.832598", "modified": "2023-06-03 16:21:54.637245",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Purchase Invoice", "name": "Purchase Invoice",

View File

@ -520,6 +520,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -2154,7 +2155,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2023-04-28 14:15:59.901154", "modified": "2023-06-03 16:22:16.219333",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -3,8 +3,10 @@
import frappe import frappe
from frappe import _ from frappe import _, qb
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder import Criterion
from frappe.query_builder.functions import Abs, Sum
from frappe.utils import cint, getdate from frappe.utils import cint, getdate
@ -346,26 +348,33 @@ def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"):
def get_advance_vouchers( def get_advance_vouchers(
parties, company=None, from_date=None, to_date=None, party_type="Supplier" parties, company=None, from_date=None, to_date=None, party_type="Supplier"
): ):
# for advance vouchers, debit and credit is reversed """
dr_or_cr = "debit" if party_type == "Supplier" else "credit" Use Payment Ledger to fetch unallocated Advance Payments
"""
filters = { ple = qb.DocType("Payment Ledger Entry")
dr_or_cr: [">", 0],
"is_opening": "No",
"is_cancelled": 0,
"party_type": party_type,
"party": ["in", parties],
}
if party_type == "Customer": conditions = []
filters.update({"against_voucher": ["is", "not set"]})
conditions.append(ple.amount.lt(0))
conditions.append(ple.delinked == 0)
conditions.append(ple.party_type == party_type)
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
if company: if company:
filters["company"] = company conditions.append(ple.company == company)
if from_date and to_date:
filters["posting_date"] = ["between", (from_date, to_date)]
return frappe.get_all("GL Entry", filters=filters, distinct=1, pluck="voucher_no") or [""] if from_date and to_date:
conditions.append(ple.posting_date[from_date:to_date])
advances = (
qb.from_(ple).select(ple.voucher_no).distinct().where(Criterion.all(conditions)).run(as_list=1)
)
if advances:
advances = [x[0] for x in advances]
return advances
def get_taxes_deducted_on_advances_allocated(inv, tax_details): def get_taxes_deducted_on_advances_allocated(inv, tax_details):
@ -499,6 +508,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
tcs_amount = 0 tcs_amount = 0
ple = qb.DocType("Payment Ledger Entry")
# sum of debit entries made from sales invoices # sum of debit entries made from sales invoices
invoiced_amt = ( invoiced_amt = (
@ -516,18 +526,20 @@ def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
) )
# sum of credit entries made from PE / JV with unset 'against voucher' # sum of credit entries made from PE / JV with unset 'against voucher'
conditions = []
conditions.append(ple.amount.lt(0))
conditions.append(ple.delinked == 0)
conditions.append(ple.party.isin(parties))
conditions.append(ple.voucher_no == ple.against_voucher_no)
conditions.append(ple.company == inv.company)
advances = (
qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run(as_list=1)
)
advance_amt = ( advance_amt = (
frappe.db.get_value( qb.from_(ple).select(Abs(Sum(ple.amount))).where(Criterion.all(conditions)).run()[0][0] or 0.0
"GL Entry",
{
"is_cancelled": 0,
"party": ["in", parties],
"company": inv.company,
"voucher_no": ["in", adv_vouchers],
},
"sum(credit)",
)
or 0.0
) )
# sum of credit entries made from sales invoice # sum of credit entries made from sales invoice

View File

@ -152,6 +152,60 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in reversed(invoices): for d in reversed(invoices):
d.cancel() d.cancel()
def test_tcs_on_unallocated_advance_payments(self):
frappe.db.set_value(
"Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS"
)
vouchers = []
# create advance payment
pe = create_payment_entry(
payment_type="Receive", party_type="Customer", party="Test TCS Customer", paid_amount=20000
)
pe.paid_from = "Debtors - _TC"
pe.paid_to = "Cash - _TC"
pe.submit()
vouchers.append(pe)
# create invoice
si1 = create_sales_invoice(customer="Test TCS Customer", rate=5000)
si1.submit()
vouchers.append(si1)
# reconcile
pr = frappe.get_doc("Payment Reconciliation")
pr.company = "_Test Company"
pr.party_type = "Customer"
pr.party = "Test TCS Customer"
pr.receivable_payable_account = "Debtors - _TC"
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# make another invoice
# sum of unallocated amount from payment entry and this sales invoice will breach cumulative threashold
# TDS should be calculated
si2 = create_sales_invoice(customer="Test TCS Customer", rate=15000)
si2.submit()
vouchers.append(si2)
si3 = create_sales_invoice(customer="Test TCS Customer", rate=10000)
si3.submit()
vouchers.append(si3)
# assert tax collection on total invoice amount created until now
tcs_charged = sum([d.base_tax_amount for d in si2.taxes if d.account_head == "TCS - _TC"])
tcs_charged += sum([d.base_tax_amount for d in si3.taxes if d.account_head == "TCS - _TC"])
self.assertEqual(tcs_charged, 1500)
# cancel invoice and payments to avoid clashing
for d in reversed(vouchers):
d.reload()
d.cancel()
def test_tds_calculation_on_net_total(self): def test_tds_calculation_on_net_total(self):
frappe.db.set_value( frappe.db.set_value(
"Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS"

View File

@ -2,6 +2,8 @@
# License: GNU General Public License v3. See license.txt # License: GNU General Public License v3. See license.txt
from typing import Optional
import frappe import frappe
from frappe import _, msgprint, scrub from frappe import _, msgprint, scrub
from frappe.contacts.doctype.address.address import ( from frappe.contacts.doctype.address.address import (
@ -647,12 +649,12 @@ def set_taxes(
else: else:
args.update(get_party_details(party, party_type)) args.update(get_party_details(party, party_type))
if party_type in ("Customer", "Lead"): if party_type in ("Customer", "Lead", "Prospect"):
args.update({"tax_type": "Sales"}) args.update({"tax_type": "Sales"})
if party_type == "Lead": if party_type in ["Lead", "Prospect"]:
args["customer"] = None args["customer"] = None
del args["lead"] del args[frappe.scrub(party_type)]
else: else:
args.update({"tax_type": "Purchase"}) args.update({"tax_type": "Purchase"})
@ -850,7 +852,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None):
return company_wise_info return company_wise_info
def get_party_shipping_address(doctype, name): def get_party_shipping_address(doctype: str, name: str) -> Optional[str]:
""" """
Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true. Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true.
and/or `is_shipping_address = 1`. and/or `is_shipping_address = 1`.
@ -861,22 +863,23 @@ def get_party_shipping_address(doctype, name):
:param name: Party name :param name: Party name
:return: String :return: String
""" """
out = frappe.db.sql( shipping_addresses = frappe.get_all(
"SELECT dl.parent " "Address",
"from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name " filters=[
"where " ["Dynamic Link", "link_doctype", "=", doctype],
"dl.link_doctype=%s " ["Dynamic Link", "link_name", "=", name],
"and dl.link_name=%s " ["disabled", "=", 0],
"and dl.parenttype='Address' " ],
"and ifnull(ta.disabled, 0) = 0 and" or_filters=[
"(ta.address_type='Shipping' or ta.is_shipping_address=1) " ["is_shipping_address", "=", 1],
"order by ta.is_shipping_address desc, ta.address_type desc limit 1", ["address_type", "=", "Shipping"],
(doctype, name), ],
pluck="name",
limit=1,
order_by="is_shipping_address DESC",
) )
if out:
return out[0][0] return shipping_addresses[0] if shipping_addresses else None
else:
return ""
def get_partywise_advanced_payment_amount( def get_partywise_advanced_payment_amount(
@ -910,31 +913,32 @@ def get_partywise_advanced_payment_amount(
return frappe._dict(data) return frappe._dict(data)
def get_default_contact(doctype, name): def get_default_contact(doctype: str, name: str) -> Optional[str]:
""" """
Returns default contact for the given doctype and name. Returns contact name only if there is a primary contact for given doctype and name.
Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact.
Else returns None
:param doctype: Party Doctype
:param name: Party name
:return: String
""" """
out = frappe.db.sql( contacts = frappe.get_all(
""" "Contact",
SELECT dl.parent, c.is_primary_contact, c.is_billing_contact filters=[
FROM `tabDynamic Link` dl ["Dynamic Link", "link_doctype", "=", doctype],
INNER JOIN `tabContact` c ON c.name = dl.parent ["Dynamic Link", "link_name", "=", name],
WHERE ],
dl.link_doctype=%s AND or_filters=[
dl.link_name=%s AND ["is_primary_contact", "=", 1],
dl.parenttype = 'Contact' ["is_billing_contact", "=", 1],
ORDER BY is_primary_contact DESC, is_billing_contact DESC ],
""", pluck="name",
(doctype, name), limit=1,
order_by="is_primary_contact DESC, is_billing_contact DESC",
) )
if out:
try: return contacts[0] if contacts else None
return out[0][0]
except Exception:
return None
else:
return None
def add_party_account(party_type, party, company, account): def add_party_account(party_type, party, company, account):

View File

@ -181,6 +181,16 @@ class ReceivablePayableReport(object):
return return
key = (ple.against_voucher_type, ple.against_voucher_no, ple.party) key = (ple.against_voucher_type, ple.against_voucher_no, ple.party)
# If payment is made against credit note
# and credit note is made against a Sales Invoice
# then consider the payment against original sales invoice.
if ple.against_voucher_type in ("Sales Invoice", "Purchase Invoice"):
if ple.against_voucher_no in self.return_entries:
return_against = self.return_entries.get(ple.against_voucher_no)
if return_against:
key = (ple.against_voucher_type, return_against, ple.party)
row = self.voucher_balance.get(key) row = self.voucher_balance.get(key)
if not row: if not row:
@ -610,7 +620,7 @@ class ReceivablePayableReport(object):
def get_return_entries(self): def get_return_entries(self):
doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
filters = {"is_return": 1, "docstatus": 1} filters = {"is_return": 1, "docstatus": 1, "company": self.filters.company}
party_field = scrub(self.filters.party_type) party_field = scrub(self.filters.party_type)
if self.filters.get(party_field): if self.filters.get(party_field):
filters.update({party_field: self.filters.get(party_field)}) filters.update({party_field: self.filters.get(party_field)})

View File

@ -210,6 +210,67 @@ class TestAccountsReceivable(FrappeTestCase):
], ],
) )
def test_payment_against_credit_note(self):
"""
Payment against credit/debit note should be considered against the parent invoice
"""
company = "_Test Company 2"
customer = "_Test Customer 2"
si1 = make_sales_invoice()
pe = get_payment_entry("Sales Invoice", si1.name, bank_account="Cash - _TC2")
pe.paid_from = "Debtors - _TC2"
pe.insert()
pe.submit()
cr_note = make_credit_note(si1.name)
si2 = make_sales_invoice()
# manually link cr_note with si2 using journal entry
je = frappe.new_doc("Journal Entry")
je.company = company
je.voucher_type = "Credit Note"
je.posting_date = today()
debit_account = "Debtors - _TC2"
debit_entry = {
"account": debit_account,
"party_type": "Customer",
"party": customer,
"debit": 100,
"debit_in_account_currency": 100,
"reference_type": cr_note.doctype,
"reference_name": cr_note.name,
"cost_center": "Main - _TC2",
}
credit_entry = {
"account": debit_account,
"party_type": "Customer",
"party": customer,
"credit": 100,
"credit_in_account_currency": 100,
"reference_type": si2.doctype,
"reference_name": si2.name,
"cost_center": "Main - _TC2",
}
je.append("accounts", debit_entry)
je.append("accounts", credit_entry)
je = je.save().submit()
filters = {
"company": company,
"report_date": today(),
"range1": 30,
"range2": 60,
"range3": 90,
"range4": 120,
}
report = execute(filters)
self.assertEqual(report[1], [])
def make_sales_invoice(no_payment_schedule=False, do_not_submit=False): def make_sales_invoice(no_payment_schedule=False, do_not_submit=False):
frappe.set_user("Administrator") frappe.set_user("Administrator")
@ -256,7 +317,7 @@ def make_payment(docname):
def make_credit_note(docname): def make_credit_note(docname):
create_sales_invoice( credit_note = create_sales_invoice(
company="_Test Company 2", company="_Test Company 2",
customer="_Test Customer 2", customer="_Test Customer 2",
currency="EUR", currency="EUR",
@ -269,3 +330,5 @@ def make_credit_note(docname):
is_return=1, is_return=1,
return_against=docname, return_against=docname,
) )
return credit_note

View File

@ -399,8 +399,9 @@ def get_items(filters, additional_query_columns, additional_conditions=None):
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
`tabSales Invoice`.unrealized_profit_loss_account, `tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.is_internal_customer, `tabSales Invoice`.is_internal_customer,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total, `tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
`tabSales Invoice Item`.project,
`tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description, `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
`tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`, `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
`tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,

View File

@ -812,14 +812,14 @@ class TestDepreciationMethods(AssetSetup):
number_of_depreciations_booked=1, number_of_depreciations_booked=1,
opening_accumulated_depreciation=50000, opening_accumulated_depreciation=50000,
expected_value_after_useful_life=10000, expected_value_after_useful_life=10000,
depreciation_start_date="2030-12-31", depreciation_start_date="2031-12-31",
total_number_of_depreciations=3, total_number_of_depreciations=3,
frequency_of_depreciation=12, frequency_of_depreciation=12,
) )
self.assertEqual(asset.status, "Draft") self.assertEqual(asset.status, "Draft")
expected_schedules = [["2030-12-31", 33333.50, 83333.50], ["2031-12-31", 6666.50, 90000.0]] expected_schedules = [["2031-12-31", 33333.50, 83333.50], ["2032-12-31", 6666.50, 90000.0]]
schedules = [ schedules = [
[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] [cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount]

View File

@ -10,6 +10,7 @@ from frappe.utils import (
cint, cint,
date_diff, date_diff,
flt, flt,
get_first_day,
get_last_day, get_last_day,
getdate, getdate,
is_last_day_of_the_month, is_last_day_of_the_month,
@ -271,8 +272,14 @@ class AssetDepreciationSchedule(Document):
break break
# For first row # For first row
if n == 0 and has_pro_rata and not self.opening_accumulated_depreciation: if (
from_date = add_days(asset_doc.available_for_use_date, -1) n == 0
and (has_pro_rata or has_wdv_or_dd_non_yearly_pro_rata)
and not self.opening_accumulated_depreciation
):
from_date = add_days(
asset_doc.available_for_use_date, -1
) # needed to calc depr amount for available_for_use_date too
depreciation_amount, days, months = _get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, row,
depreciation_amount, depreciation_amount,
@ -281,10 +288,18 @@ class AssetDepreciationSchedule(Document):
has_wdv_or_dd_non_yearly_pro_rata, has_wdv_or_dd_non_yearly_pro_rata,
) )
elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation: elif n == 0 and has_wdv_or_dd_non_yearly_pro_rata and self.opening_accumulated_depreciation:
from_date = add_months( if not is_first_day_of_the_month(getdate(asset_doc.available_for_use_date)):
getdate(asset_doc.available_for_use_date), from_date = get_last_day(
(self.number_of_depreciations_booked * row.frequency_of_depreciation), add_months(
) getdate(asset_doc.available_for_use_date),
((self.number_of_depreciations_booked - 1) * row.frequency_of_depreciation),
)
)
else:
from_date = add_months(
getdate(add_days(asset_doc.available_for_use_date, -1)),
(self.number_of_depreciations_booked * row.frequency_of_depreciation),
)
depreciation_amount, days, months = _get_pro_rata_amt( depreciation_amount, days, months = _get_pro_rata_amt(
row, row,
depreciation_amount, depreciation_amount,
@ -702,3 +717,9 @@ def get_asset_depr_schedule_name(asset_name, status, finance_book=None):
["status", "=", status], ["status", "=", status],
], ],
) )
def is_first_day_of_the_month(date):
first_day_of_the_month = get_first_day(date)
return getdate(first_day_of_the_month) == getdate(date)

View File

@ -322,6 +322,7 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"label": "Customer Mobile No", "label": "Customer Mobile No",
"options": "Phone",
"print_hide": 1 "print_hide": 1
}, },
{ {
@ -368,6 +369,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact Mobile No", "label": "Contact Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1271,7 +1273,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-05-24 11:16:41.195340", "modified": "2023-06-03 16:19:45.710444",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order", "name": "Purchase Order",

View File

@ -230,6 +230,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -844,7 +845,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-14 16:43:41.714832", "modified": "2023-06-03 16:20:15.880114",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Supplier Quotation", "name": "Supplier Quotation",

View File

@ -43,7 +43,6 @@ class SellingController(StockController):
self.set_serial_and_batch_bundle(table_field) self.set_serial_and_batch_bundle(table_field)
def set_missing_values(self, for_validate=False): def set_missing_values(self, for_validate=False):
super(SellingController, self).set_missing_values(for_validate) super(SellingController, self).set_missing_values(for_validate)
# set contact and address details for customer, if they are not mentioned # set contact and address details for customer, if they are not mentioned
@ -62,7 +61,7 @@ class SellingController(StockController):
elif self.doctype == "Quotation" and self.party_name: elif self.doctype == "Quotation" and self.party_name:
if self.quotation_to == "Customer": if self.quotation_to == "Customer":
customer = self.party_name customer = self.party_name
else: elif self.quotation_to == "Lead":
lead = self.party_name lead = self.party_name
if customer: if customer:

View File

@ -3,7 +3,10 @@
import frappe import frappe
from frappe import _ from frappe import _
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import (
delete_contact_and_address,
load_address_and_contact,
)
from frappe.email.inbox import link_communication_to_document from frappe.email.inbox import link_communication_to_document
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address from frappe.utils import comma_and, get_link_to_form, has_gravatar, validate_email_address
@ -40,9 +43,8 @@ class Lead(SellingController, CRMNote):
self.update_prospect() self.update_prospect()
def on_trash(self): def on_trash(self):
frappe.db.sql("""update `tabIssue` set lead='' where lead=%s""", self.name) frappe.db.set_value("Issue", {"lead": self.name}, "lead", None)
delete_contact_and_address(self.doctype, self.name)
self.unlink_dynamic_links()
self.remove_link_from_prospect() self.remove_link_from_prospect()
def set_full_name(self): def set_full_name(self):
@ -119,27 +121,6 @@ class Lead(SellingController, CRMNote):
) )
lead_row.db_update() lead_row.db_update()
def unlink_dynamic_links(self):
links = frappe.get_all(
"Dynamic Link",
filters={"link_doctype": self.doctype, "link_name": self.name},
fields=["parent", "parenttype"],
)
for link in links:
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
if len(linked_doc.get("links")) == 1:
linked_doc.delete(ignore_permissions=True)
else:
to_remove = None
for d in linked_doc.get("links"):
if d.link_doctype == self.doctype and d.link_name == self.name:
to_remove = d
if to_remove:
linked_doc.remove(to_remove)
linked_doc.save(ignore_permissions=True)
def remove_link_from_prospect(self): def remove_link_from_prospect(self):
prospects = self.get_linked_prospects() prospects = self.get_linked_prospects()

View File

@ -2,7 +2,10 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe import frappe
from frappe.contacts.address_and_contact import load_address_and_contact from frappe.contacts.address_and_contact import (
delete_contact_and_address,
load_address_and_contact,
)
from frappe.model.mapper import get_mapped_doc from frappe.model.mapper import get_mapped_doc
from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events from erpnext.crm.utils import CRMNote, copy_comments, link_communications, link_open_events
@ -16,7 +19,7 @@ class Prospect(CRMNote):
self.link_with_lead_contact_and_address() self.link_with_lead_contact_and_address()
def on_trash(self): def on_trash(self):
self.unlink_dynamic_links() delete_contact_and_address(self.doctype, self.name)
def after_insert(self): def after_insert(self):
carry_forward_communication_and_comments = frappe.db.get_single_value( carry_forward_communication_and_comments = frappe.db.get_single_value(
@ -54,27 +57,6 @@ class Prospect(CRMNote):
linked_doc.append("links", {"link_doctype": self.doctype, "link_name": self.name}) linked_doc.append("links", {"link_doctype": self.doctype, "link_name": self.name})
linked_doc.save(ignore_permissions=True) linked_doc.save(ignore_permissions=True)
def unlink_dynamic_links(self):
links = frappe.get_all(
"Dynamic Link",
filters={"link_doctype": self.doctype, "link_name": self.name},
fields=["parent", "parenttype"],
)
for link in links:
linked_doc = frappe.get_doc(link["parenttype"], link["parent"])
if len(linked_doc.get("links")) == 1:
linked_doc.delete(ignore_permissions=True)
else:
to_remove = None
for d in linked_doc.get("links"):
if d.link_doctype == self.doctype and d.link_name == self.name:
to_remove = d
if to_remove:
linked_doc.remove(to_remove)
linked_doc.save(ignore_permissions=True)
@frappe.whitelist() @frappe.whitelist()
def make_customer(source_name, target_doc=None): def make_customer(source_name, target_doc=None):

View File

@ -78,9 +78,10 @@ erpnext.ProductList = class {
let title_html = `<div style="display: flex; margin-left: -15px;">`; let title_html = `<div style="display: flex; margin-left: -15px;">`;
title_html += ` title_html += `
<div class="col-8" style="margin-right: -15px;"> <div class="col-8" style="margin-right: -15px;">
<a class="" href="/${ item.route || '#' }" <a href="/${ item.route || '#' }">
style="color: var(--gray-800); font-weight: 500;"> <div class="product-title">
${ title } ${ title }
</div>
</a> </a>
</div> </div>
`; `;
@ -201,4 +202,4 @@ erpnext.ProductList = class {
} }
} }
}; };

View File

@ -160,4 +160,3 @@ class TestLoanDisbursement(unittest.TestCase):
interest = per_day_interest * 15 interest = per_day_interest * 15
self.assertEqual(amounts["pending_principal_amount"], 1500000) self.assertEqual(amounts["pending_principal_amount"], 1500000)
self.assertEqual(amounts["interest_amount"], flt(interest + previous_interest, 2))

View File

@ -22,7 +22,7 @@ class LoanInterestAccrual(AccountsController):
frappe.throw(_("Interest Amount or Principal Amount is mandatory")) frappe.throw(_("Interest Amount or Principal Amount is mandatory"))
if not self.last_accrual_date: if not self.last_accrual_date:
self.last_accrual_date = get_last_accrual_date(self.loan) self.last_accrual_date = get_last_accrual_date(self.loan, self.posting_date)
def on_submit(self): def on_submit(self):
self.make_gl_entries() self.make_gl_entries()
@ -274,14 +274,14 @@ def make_loan_interest_accrual_entry(args):
def get_no_of_days_for_interest_accural(loan, posting_date): def get_no_of_days_for_interest_accural(loan, posting_date):
last_interest_accrual_date = get_last_accrual_date(loan.name) last_interest_accrual_date = get_last_accrual_date(loan.name, posting_date)
no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1 no_of_days = date_diff(posting_date or nowdate(), last_interest_accrual_date) + 1
return no_of_days return no_of_days
def get_last_accrual_date(loan): def get_last_accrual_date(loan, posting_date):
last_posting_date = frappe.db.sql( last_posting_date = frappe.db.sql(
""" SELECT MAX(posting_date) from `tabLoan Interest Accrual` """ SELECT MAX(posting_date) from `tabLoan Interest Accrual`
WHERE loan = %s and docstatus = 1""", WHERE loan = %s and docstatus = 1""",
@ -289,12 +289,30 @@ def get_last_accrual_date(loan):
) )
if last_posting_date[0][0]: if last_posting_date[0][0]:
last_interest_accrual_date = last_posting_date[0][0]
# interest for last interest accrual date is already booked, so add 1 day # interest for last interest accrual date is already booked, so add 1 day
return add_days(last_posting_date[0][0], 1) last_disbursement_date = get_last_disbursement_date(loan, posting_date)
if last_disbursement_date and getdate(last_disbursement_date) > getdate(
last_interest_accrual_date
):
last_interest_accrual_date = last_disbursement_date
return add_days(last_interest_accrual_date, 1)
else: else:
return frappe.db.get_value("Loan", loan, "disbursement_date") return frappe.db.get_value("Loan", loan, "disbursement_date")
def get_last_disbursement_date(loan, posting_date):
last_disbursement_date = frappe.db.get_value(
"Loan Disbursement",
{"docstatus": 1, "against_loan": loan, "posting_date": ("<", posting_date)},
"MAX(posting_date)",
)
return last_disbursement_date
def days_in_year(year): def days_in_year(year):
days = 365 days = 365

View File

@ -101,7 +101,7 @@ class LoanRepayment(AccountsController):
if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision): if flt(self.total_interest_paid, precision) > flt(self.interest_payable, precision):
if not self.is_term_loan: if not self.is_term_loan:
# get last loan interest accrual date # get last loan interest accrual date
last_accrual_date = get_last_accrual_date(self.against_loan) last_accrual_date = get_last_accrual_date(self.against_loan, self.posting_date)
# get posting date upto which interest has to be accrued # get posting date upto which interest has to be accrued
per_day_interest = get_per_day_interest( per_day_interest = get_per_day_interest(
@ -725,7 +725,7 @@ def get_amounts(amounts, against_loan, posting_date):
if due_date: if due_date:
pending_days = date_diff(posting_date, due_date) + 1 pending_days = date_diff(posting_date, due_date) + 1
else: else:
last_accrual_date = get_last_accrual_date(against_loan_doc.name) last_accrual_date = get_last_accrual_date(against_loan_doc.name, posting_date)
pending_days = date_diff(posting_date, last_accrual_date) + 1 pending_days = date_diff(posting_date, last_accrual_date) + 1
if pending_days > 0: if pending_days > 0:

View File

@ -152,6 +152,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -160,6 +161,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
@ -236,10 +238,11 @@
"link_fieldname": "maintenance_schedule" "link_fieldname": "maintenance_schedule"
} }
], ],
"modified": "2021-05-27 16:05:10.746465", "modified": "2023-06-03 16:15:43.958072",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Schedule", "name": "Maintenance Schedule",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -260,5 +263,6 @@
"search_fields": "status,customer,customer_name", "search_fields": "status,customer,customer_name",
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"timeline_field": "customer" "timeline_field": "customer"
} }

View File

@ -101,6 +101,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -108,6 +109,7 @@
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 1, "hidden": 1,
"label": "Contact Email", "label": "Contact Email",
"options": "Email",
"read_only": 1 "read_only": 1
}, },
{ {
@ -293,7 +295,7 @@
"idx": 1, "idx": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-12-17 03:10:27.608112", "modified": "2023-06-03 16:19:07.902723",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Maintenance", "module": "Maintenance",
"name": "Maintenance Visit", "name": "Maintenance Visit",
@ -319,6 +321,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"timeline_field": "customer", "timeline_field": "customer",
"title_field": "customer_name" "title_field": "customer_name"
} }

View File

@ -88,12 +88,14 @@ class BOMUpdateLog(Document):
boms=boms, boms=boms,
timeout=40000, timeout=40000,
now=frappe.flags.in_test, now=frappe.flags.in_test,
enqueue_after_commit=True,
) )
else: else:
frappe.enqueue( frappe.enqueue(
method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise", method="erpnext.manufacturing.doctype.bom_update_log.bom_update_log.process_boms_cost_level_wise",
update_doc=self, update_doc=self,
now=frappe.flags.in_test, now=frappe.flags.in_test,
enqueue_after_commit=True,
) )

View File

@ -304,6 +304,7 @@ def set_tasks_as_overdue():
@frappe.whitelist() @frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False): def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
def set_missing_values(source, target): def set_missing_values(source, target):
target.parent_project = source.project
target.append( target.append(
"time_logs", "time_logs",
{ {

View File

@ -40,8 +40,8 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
name: __("Date"), name: __("Date"),
editable: false, editable: false,
width: 100, width: 100,
format: frappe.form.formatters.Date,
}, },
{ {
name: __("Party Type"), name: __("Party Type"),
editable: false, editable: false,
@ -117,17 +117,13 @@ erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
return [ return [
row["date"], row["date"],
row["party_type"], row["party_type"],
row["party"], frappe.form.formatters.Link(row["party"], {options: row["party_type"]}),
row["description"], row["description"],
row["deposit"], row["deposit"],
row["withdrawal"], row["withdrawal"],
row["unallocated_amount"], row["unallocated_amount"],
row["reference_number"], row["reference_number"],
` `<button class="btn btn-primary btn-xs center" data-name="${row["name"]}">${__("Actions")}</button>`
<Button class="btn btn-primary btn-xs center" data-name = ${row["name"]} >
${__("Actions")}
</a>
`,
]; ];
} }

View File

@ -76,30 +76,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
callback: (result) => { callback: (result) => {
const data = result.message; const data = result.message;
if (data && data.length > 0) { if (data && data.length > 0) {
const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper; const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
proposals_wrapper.show(); proposals_wrapper.show();
this.dialog.fields_dict.no_matching_vouchers.$wrapper.hide(); this.dialog.fields_dict.no_matching_vouchers.$wrapper.hide();
this.data = []; this.data = data.map((row) => this.format_row(row));
data.forEach((row) => {
const reference_date = row[5] ? row[5] : row[8];
this.data.push([
row[1],
row[2],
reference_date,
format_currency(row[3], row[9]),
row[4],
row[6],
]);
});
this.get_dt_columns(); this.get_dt_columns();
this.get_datatable(proposals_wrapper); this.get_datatable(proposals_wrapper);
} else { } else {
const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper; const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
proposals_wrapper.hide(); proposals_wrapper.hide();
this.dialog.fields_dict.no_matching_vouchers.$wrapper.show(); this.dialog.fields_dict.no_matching_vouchers.$wrapper.show();
} }
this.dialog.show(); this.dialog.show();
}, },
@ -122,6 +109,7 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
name: __("Reference Date"), name: __("Reference Date"),
editable: false, editable: false,
width: 120, width: 120,
format: frappe.form.formatters.Date,
}, },
{ {
name: __("Remaining"), name: __("Remaining"),
@ -141,6 +129,17 @@ erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
]; ];
} }
format_row(row) {
return [
row[1], // Document Type
frappe.form.formatters.Link(row[2], {options: row[1]}), // Document Name
row[5] || row[8], // Reference Date
format_currency(row[3], row[9]), // Remaining
row[4], // Reference Number
row[6], // Party
];
}
get_datatable(proposals_wrapper) { get_datatable(proposals_wrapper) {
if (!this.datatable) { if (!this.datatable) {
const datatable_options = { const datatable_options = {

View File

@ -805,11 +805,13 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments {
); );
} }
this.frm.doc.payments.find(pay => { if(!this.frm.doc.is_return){
if (pay.default) { this.frm.doc.payments.find(payment => {
pay.amount = total_amount_to_pay; if (payment.default) {
} payment.amount = total_amount_to_pay;
}); }
});
}
this.frm.refresh_fields(); this.frm.refresh_fields();
} }

View File

@ -16,8 +16,8 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) {
|| (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) {
let party_type = "Customer"; let party_type = "Customer";
if (frm.doc.quotation_to && frm.doc.quotation_to === "Lead") { if (frm.doc.quotation_to && in_list(["Lead", "Prospect"], frm.doc.quotation_to)) {
party_type = "Lead"; party_type = frm.doc.quotation_to;
} }
args = { args = {

View File

@ -454,12 +454,12 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
customer_outstanding += flt(extra_amount) customer_outstanding += flt(extra_amount)
if credit_limit > 0 and flt(customer_outstanding) > credit_limit: if credit_limit > 0 and flt(customer_outstanding) > credit_limit:
msgprint( message = _("Credit limit has been crossed for customer {0} ({1}/{2})").format(
_("Credit limit has been crossed for customer {0} ({1}/{2})").format( customer, customer_outstanding, credit_limit
customer, customer_outstanding, credit_limit
)
) )
message += "<br><br>"
# If not authorized person raise exception # If not authorized person raise exception
credit_controller_role = frappe.db.get_single_value("Accounts Settings", "credit_controller") credit_controller_role = frappe.db.get_single_value("Accounts Settings", "credit_controller")
if not credit_controller_role or credit_controller_role not in frappe.get_roles(): if not credit_controller_role or credit_controller_role not in frappe.get_roles():
@ -480,7 +480,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
"<li>".join(credit_controller_users_formatted) "<li>".join(credit_controller_users_formatted)
) )
message = _( message += _(
"Please contact any of the following users to extend the credit limits for {0}: {1}" "Please contact any of the following users to extend the credit limits for {0}: {1}"
).format(customer, user_list) ).format(customer, user_list)
@ -488,7 +488,7 @@ def check_credit_limit(customer, company, ignore_outstanding_sales_order=False,
# prompt them to send out an email to the controller users # prompt them to send out an email to the controller users
frappe.msgprint( frappe.msgprint(
message, message,
title="Notify", title=_("Credit Limit Crossed"),
raise_exception=1, raise_exception=1,
primary_action={ primary_action={
"label": "Send Email", "label": "Send Email",
@ -519,7 +519,6 @@ def get_customer_outstanding(
customer, company, ignore_outstanding_sales_order=False, cost_center=None customer, company, ignore_outstanding_sales_order=False, cost_center=None
): ):
# Outstanding based on GL Entries # Outstanding based on GL Entries
cond = "" cond = ""
if cost_center: if cost_center:
lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"]) lft, rgt = frappe.get_cached_value("Cost Center", cost_center, ["lft", "rgt"])

View File

@ -13,7 +13,7 @@ frappe.ui.form.on('Quotation', {
frm.set_query("quotation_to", function() { frm.set_query("quotation_to", function() {
return{ return{
"filters": { "filters": {
"name": ["in", ["Customer", "Lead"]], "name": ["in", ["Customer", "Lead", "Prospect"]],
} }
} }
}); });
@ -160,19 +160,16 @@ erpnext.selling.QuotationController = class QuotationController extends erpnext.
} }
set_dynamic_field_label(){ set_dynamic_field_label(){
if (this.frm.doc.quotation_to == "Customer") if (this.frm.doc.quotation_to == "Customer") {
{
this.frm.set_df_property("party_name", "label", "Customer"); this.frm.set_df_property("party_name", "label", "Customer");
this.frm.fields_dict.party_name.get_query = null; this.frm.fields_dict.party_name.get_query = null;
} } else if (this.frm.doc.quotation_to == "Lead") {
if (this.frm.doc.quotation_to == "Lead")
{
this.frm.set_df_property("party_name", "label", "Lead"); this.frm.set_df_property("party_name", "label", "Lead");
this.frm.fields_dict.party_name.get_query = function() { this.frm.fields_dict.party_name.get_query = function() {
return{ query: "erpnext.controllers.queries.lead_query" } return{ query: "erpnext.controllers.queries.lead_query" }
} }
} else if (this.frm.doc.quotation_to == "Prospect") {
this.frm.set_df_property("party_name", "label", "Prospect");
} }
} }

View File

@ -291,6 +291,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1072,7 +1073,7 @@
"idx": 82, "idx": 82,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-14 16:50:44.550098", "modified": "2023-06-03 16:21:04.980033",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Quotation", "name": "Quotation",

View File

@ -398,6 +398,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1475,6 +1476,7 @@
"hide_days": 1, "hide_days": 1,
"hide_seconds": 1, "hide_seconds": 1,
"label": "Phone", "label": "Phone",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1643,7 +1645,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-22 09:55:37.008190", "modified": "2023-06-03 16:16:23.411247",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@ -230,6 +230,7 @@ class SalesOrder(SellingController):
frappe.throw(_("Quotation {0} is cancelled").format(quotation)) frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True) doc.set_status(update=True)
doc.update_opportunity("Converted" if flag == "submit" else "Quotation")
def validate_drop_ship(self): def validate_drop_ship(self):
for d in self.get("items"): for d in self.get("items"):

View File

@ -1772,7 +1772,14 @@ class TestSalesOrder(FrappeTestCase):
self.assertEqual(pe.references[1].reference_name, so.name) self.assertEqual(pe.references[1].reference_name, so.name)
self.assertEqual(pe.references[1].allocated_amount, 300) self.assertEqual(pe.references[1].allocated_amount, 300)
@change_settings("Stock Settings", {"enable_stock_reservation": 1}) @change_settings(
"Stock Settings",
{
"enable_stock_reservation": 1,
"auto_create_serial_and_batch_bundle_for_outward": 1,
"pick_serial_and_batch_based_on": "FIFO",
},
)
def test_stock_reservation_against_sales_order(self) -> None: def test_stock_reservation_against_sales_order(self) -> None:
from random import randint, uniform from random import randint, uniform

View File

@ -25,6 +25,7 @@ def after_install():
create_default_success_action() create_default_success_action()
create_default_energy_point_rules() create_default_energy_point_rules()
create_incoterms() create_incoterms()
create_default_role_profiles()
add_company_to_session_defaults() add_company_to_session_defaults()
add_standard_navbar_items() add_standard_navbar_items()
add_app_name() add_app_name()
@ -202,3 +203,42 @@ def setup_log_settings():
def hide_workspaces(): def hide_workspaces():
for ws in ["Integration", "Settings"]: for ws in ["Integration", "Settings"]:
frappe.db.set_value("Workspace", ws, "public", 0) frappe.db.set_value("Workspace", ws, "public", 0)
def create_default_role_profiles():
for role_profile_name, roles in DEFAULT_ROLE_PROFILES.items():
role_profile = frappe.new_doc("Role Profile")
role_profile.role_profile = role_profile_name
for role in roles:
role_profile.append("roles", {"role": role})
role_profile.insert(ignore_permissions=True)
DEFAULT_ROLE_PROFILES = {
"Inventory": [
"Stock User",
"Stock Manager",
"Item Manager",
],
"Manufacturing": [
"Stock User",
"Manufacturing User",
"Manufacturing Manager",
],
"Accounts": [
"Accounts User",
"Accounts Manager",
],
"Sales": [
"Sales User",
"Stock User",
"Sales Manager",
],
"Purchase": [
"Item Manager",
"Stock User",
"Purchase User",
"Purchase Manager",
],
}

View File

@ -51,7 +51,7 @@ class ClosingStockBalance(Document):
for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]:
if self.get(fieldname): if self.get(fieldname):
query = query.where(table.get(fieldname) == self.get(fieldname)) query = query.where(table[fieldname] == self.get(fieldname))
query = query.run(as_dict=True) query = query.run(as_dict=True)

View File

@ -374,6 +374,7 @@
"fieldtype": "Small Text", "fieldtype": "Small Text",
"hidden": 1, "hidden": 1,
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1398,7 +1399,7 @@
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-21 11:15:23.931084", "modified": "2023-06-03 16:13:25.011487",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",
@ -1468,4 +1469,4 @@
"title_field": "title", "title_field": "title",
"track_changes": 1, "track_changes": 1,
"track_seen": 1 "track_seen": 1
} }

View File

@ -772,12 +772,6 @@ $.extend(erpnext.item, {
if (modal) { if (modal) {
$(modal).removeClass("modal-dialog-scrollable"); $(modal).removeClass("modal-dialog-scrollable");
} }
})
.on("awesomplete-close", () => {
let modal = field.$input.parents('.modal-dialog')[0];
if (modal) {
$(modal).addClass("modal-dialog-scrollable");
}
}); });
}); });
}, },

View File

@ -714,6 +714,7 @@ class Item(Document):
template=self, template=self,
now=frappe.flags.in_test, now=frappe.flags.in_test,
timeout=600, timeout=600,
enqueue_after_commit=True,
) )
def validate_has_variants(self): def validate_has_variants(self):

View File

@ -326,6 +326,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -1239,7 +1240,7 @@
"idx": 261, "idx": 261,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-05-07 20:18:25.458185", "modified": "2023-06-03 16:23:20.781368",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt", "name": "Purchase Receipt",

View File

@ -127,6 +127,14 @@ frappe.ui.form.on('Serial and Batch Bundle', {
}, },
toggle_fields(frm) { toggle_fields(frm) {
if (frm.doc.has_serial_no) {
frm.doc.entries.forEach(row => {
if (Math.abs(row.qty) !== 1) {
frappe.model.set_value(row.doctype, row.name, "qty", 1);
}
})
}
frm.fields_dict.entries.grid.update_docfield_property( frm.fields_dict.entries.grid.update_docfield_property(
'serial_no', 'read_only', !frm.doc.has_serial_no 'serial_no', 'read_only', !frm.doc.has_serial_no
); );
@ -134,6 +142,10 @@ frappe.ui.form.on('Serial and Batch Bundle', {
frm.fields_dict.entries.grid.update_docfield_property( frm.fields_dict.entries.grid.update_docfield_property(
'batch_no', 'read_only', !frm.doc.has_batch_no 'batch_no', 'read_only', !frm.doc.has_batch_no
); );
frm.fields_dict.entries.grid.update_docfield_property(
'qty', 'read_only', frm.doc.has_serial_no
);
}, },
set_queries(frm) { set_queries(frm) {
@ -198,9 +210,9 @@ frappe.ui.form.on('Serial and Batch Bundle', {
frappe.ui.form.on("Serial and Batch Entry", { frappe.ui.form.on("Serial and Batch Entry", {
ledgers_add(frm, cdt, cdn) { entries_add(frm, cdt, cdn) {
if (frm.doc.warehouse) { if (frm.doc.warehouse) {
locals[cdt][cdn].warehouse = frm.doc.warehouse; frappe.model.set_value(cdt, cdn, 'warehouse', frm.doc.warehouse);
} }
}, },
}) })

View File

@ -133,7 +133,7 @@ class SerialandBatchBundle(Document):
def calculate_total_qty(self, save=True): def calculate_total_qty(self, save=True):
self.total_qty = 0.0 self.total_qty = 0.0
for d in self.entries: for d in self.entries:
d.qty = abs(d.qty) if d.qty else 0 d.qty = 1 if self.has_serial_no and abs(d.qty) > 1 else abs(d.qty) if d.qty else 0
d.stock_value_difference = abs(d.stock_value_difference) if d.stock_value_difference else 0 d.stock_value_difference = abs(d.stock_value_difference) if d.stock_value_difference else 0
if self.type_of_transaction == "Outward": if self.type_of_transaction == "Outward":
d.qty *= -1 d.qty *= -1

View File

@ -0,0 +1,11 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.listview_settings["Serial and Batch Bundle"] = {
add_fields: ["is_cancelled"],
get_indicator: function (doc) {
if (doc.is_cancelled) {
return [__("Cancelled"), "red", "is_cancelled,=,1"];
}
},
};

View File

@ -94,6 +94,7 @@ class StockSettings(Document):
frappe.enqueue( frappe.enqueue(
"erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions", "erpnext.stock.doctype.stock_settings.stock_settings.clean_all_descriptions",
now=frappe.flags.in_test, now=frappe.flags.in_test,
enqueue_after_commit=True,
) )
def validate_pending_reposts(self): def validate_pending_reposts(self):

View File

@ -944,7 +944,7 @@ class update_entries_after(object):
for item in sr.items: for item in sr.items:
# Skip for Serial and Batch Items # Skip for Serial and Batch Items
if item.serial_no or item.batch_no: if item.name != sle.voucher_detail_no or item.serial_no or item.batch_no:
continue continue
previous_sle = get_previous_sle( previous_sle = get_previous_sle(

View File

@ -205,6 +205,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -629,7 +630,7 @@
"in_create": 1, "in_create": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2022-11-16 14:18:57.001239", "modified": "2023-06-03 16:18:39.088518",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Subcontracting", "module": "Subcontracting",
"name": "Subcontracting Receipt", "name": "Subcontracting Receipt",

View File

@ -1,9 +1,11 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"creation": "2013-01-10 16:34:30", "creation": "2013-01-10 16:34:30",
"doctype": "DocType", "doctype": "DocType",
"document_type": "Setup", "document_type": "Setup",
"engine": "InnoDB",
"field_order": [ "field_order": [
"naming_series", "naming_series",
"status", "status",
@ -249,6 +251,7 @@
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
@ -362,10 +365,12 @@
], ],
"icon": "fa fa-bug", "icon": "fa fa-bug",
"idx": 1, "idx": 1,
"modified": "2021-11-09 17:26:09.703215", "links": [],
"modified": "2023-06-03 16:17:07.694449",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Support", "module": "Support",
"name": "Warranty Claim", "name": "Warranty Claim",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@ -384,6 +389,7 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"timeline_field": "customer", "timeline_field": "customer",
"title_field": "customer_name" "title_field": "customer_name"
} }