mirror of
https://github.com/frappe/erpnext.git
synced 2026-04-14 04:15:10 +00:00
Merge branch 'frappe:develop' into fix_timesheet_invoice
This commit is contained in:
@@ -57,7 +57,9 @@ class BankAccount(Document):
|
||||
|
||||
def validate_account(self):
|
||||
if self.account:
|
||||
if accounts := frappe.db.get_all("Bank Account", filters={"account": self.account}, as_list=1):
|
||||
if accounts := frappe.db.get_all(
|
||||
"Bank Account", filters={"account": self.account, "name": ["!=", self.name]}, as_list=1
|
||||
):
|
||||
frappe.throw(
|
||||
_("'{0}' account is already used by {1}. Use another account.").format(
|
||||
frappe.bold(self.account),
|
||||
|
||||
@@ -3,6 +3,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
@@ -106,6 +107,8 @@ class PaymentRequest(Document):
|
||||
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":
|
||||
ref_amount = get_amount(ref_doc, self.payment_account)
|
||||
if not ref_amount:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
|
||||
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
|
||||
frappe.throw(
|
||||
@@ -453,6 +456,8 @@ def make_payment_request(**args):
|
||||
gateway_account = get_gateway_details(args) or frappe._dict()
|
||||
|
||||
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
|
||||
if not grand_total:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
if args.loyalty_points and args.dt == "Sales Order":
|
||||
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
|
||||
|
||||
@@ -543,6 +548,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
dt = ref_doc.doctype
|
||||
if dt in ["Sales Order", "Purchase Order"]:
|
||||
grand_total = flt(ref_doc.rounded_total) or flt(ref_doc.grand_total)
|
||||
grand_total -= get_paid_amount_against_order(dt, ref_doc.name)
|
||||
elif dt in ["Sales Invoice", "Purchase Invoice"]:
|
||||
if not ref_doc.get("is_pos"):
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
@@ -562,10 +568,7 @@ def get_amount(ref_doc, payment_account=None):
|
||||
elif dt == "Fees":
|
||||
grand_total = ref_doc.outstanding_amount
|
||||
|
||||
if grand_total > 0:
|
||||
return grand_total
|
||||
else:
|
||||
frappe.throw(_("Payment Entry is already created"))
|
||||
return grand_total
|
||||
|
||||
|
||||
def get_existing_payment_request_amount(ref_dt, ref_dn):
|
||||
@@ -748,3 +751,27 @@ def validate_payment(doc, method=None):
|
||||
doc.reference_docname
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def get_paid_amount_against_order(dt, dn):
|
||||
pe_ref = frappe.qb.DocType("Payment Entry Reference")
|
||||
if dt == "Sales Order":
|
||||
inv_dt, inv_field = "Sales Invoice Item", "sales_order"
|
||||
else:
|
||||
inv_dt, inv_field = "Purchase Invoice Item", "purchase_order"
|
||||
inv_item = frappe.qb.DocType(inv_dt)
|
||||
return (
|
||||
frappe.qb.from_(pe_ref)
|
||||
.select(
|
||||
Sum(pe_ref.allocated_amount),
|
||||
)
|
||||
.where(
|
||||
(pe_ref.docstatus == 1)
|
||||
& (
|
||||
(pe_ref.reference_name == dn)
|
||||
| pe_ref.reference_name.isin(
|
||||
frappe.qb.from_(inv_item).select(inv_item.parent).where(inv_item[inv_field] == dn).distinct()
|
||||
)
|
||||
)
|
||||
)
|
||||
).run()[0][0] or 0
|
||||
|
||||
@@ -1025,9 +1025,14 @@ class PurchaseInvoice(BuyingController):
|
||||
|
||||
if provisional_accounting_for_non_stock_items:
|
||||
if item.purchase_receipt:
|
||||
provisional_account = frappe.db.get_value(
|
||||
"Purchase Receipt Item", item.pr_detail, "provisional_expense_account"
|
||||
) or self.get_company_default("default_provisional_account")
|
||||
provisional_account, pr_qty, pr_base_rate = frappe.get_cached_value(
|
||||
"Purchase Receipt Item",
|
||||
item.pr_detail,
|
||||
["provisional_expense_account", "qty", "base_rate"],
|
||||
)
|
||||
provisional_account = provisional_account or self.get_company_default(
|
||||
"default_provisional_account"
|
||||
)
|
||||
purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt)
|
||||
|
||||
if not purchase_receipt_doc:
|
||||
@@ -1044,13 +1049,18 @@ class PurchaseInvoice(BuyingController):
|
||||
"voucher_detail_no": item.pr_detail,
|
||||
"account": provisional_account,
|
||||
},
|
||||
["name"],
|
||||
"name",
|
||||
)
|
||||
|
||||
if expense_booked_in_pr:
|
||||
# Intentionally passing purchase invoice item to handle partial billing
|
||||
purchase_receipt_doc.add_provisional_gl_entry(
|
||||
item, gl_entries, self.posting_date, provisional_account, reverse=1
|
||||
item,
|
||||
gl_entries,
|
||||
self.posting_date,
|
||||
provisional_account,
|
||||
reverse=1,
|
||||
item_amount=(min(item.qty, pr_qty) * pr_base_rate),
|
||||
)
|
||||
|
||||
if not self.is_internal_transfer():
|
||||
|
||||
@@ -1539,18 +1539,7 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self.assertEqual(payment_entry.taxes[0].allocated_amount, 0)
|
||||
|
||||
def test_provisional_accounting_entry(self):
|
||||
create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
|
||||
provisional_account = create_account(
|
||||
account_name="Provision Account",
|
||||
parent_account="Current Liabilities - _TC",
|
||||
company="_Test Company",
|
||||
)
|
||||
|
||||
company = frappe.get_doc("Company", "_Test Company")
|
||||
company.enable_provisional_accounting_for_non_stock_items = 1
|
||||
company.default_provisional_account = provisional_account
|
||||
company.save()
|
||||
setup_provisional_accounting()
|
||||
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2)
|
||||
@@ -1594,8 +1583,97 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
|
||||
)
|
||||
|
||||
company.enable_provisional_accounting_for_non_stock_items = 0
|
||||
company.save()
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
def test_provisional_accounting_entry_for_over_billing(self):
|
||||
setup_provisional_accounting()
|
||||
|
||||
# Configure Buying Settings to allow rate change
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
# Create PR: rate = 1000, qty = 5
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
|
||||
)
|
||||
|
||||
# Overbill PR: rate = 2000, qty = 10
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.items[0].qty = 10
|
||||
pi.items[0].rate = 2000
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 20000, 0, add_days(pr.posting_date, -1)],
|
||||
["Creditors - _TC", 0, 20000, add_days(pr.posting_date, -1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
|
||||
# Cancel purchase invoice to check reverse provisional entry cancellation
|
||||
pi.cancel()
|
||||
|
||||
expected_gle_for_purchase_receipt_post_pi_cancel = [
|
||||
["Provision Account - _TC", 0, 5000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 5000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(
|
||||
self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date
|
||||
)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
def test_provisional_accounting_entry_for_partial_billing(self):
|
||||
setup_provisional_accounting()
|
||||
|
||||
# Configure Buying Settings to allow rate change
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
|
||||
# Create PR: rate = 1000, qty = 5
|
||||
pr = make_purchase_receipt(
|
||||
item_code="_Test Non Stock Item", rate=1000, posting_date=add_days(nowdate(), -2)
|
||||
)
|
||||
|
||||
# Partially bill PR: rate = 500, qty = 2
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
pi.set_posting_time = 1
|
||||
pi.posting_date = add_days(pr.posting_date, -1)
|
||||
pi.items[0].qty = 2
|
||||
pi.items[0].rate = 500
|
||||
pi.items[0].expense_account = "Cost of Goods Sold - _TC"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
expected_gle = [
|
||||
["Cost of Goods Sold - _TC", 1000, 0, add_days(pr.posting_date, -1)],
|
||||
["Creditors - _TC", 0, 1000, add_days(pr.posting_date, -1)],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pi.name, expected_gle, pi.posting_date)
|
||||
|
||||
expected_gle_for_purchase_receipt = [
|
||||
["Provision Account - _TC", 5000, 0, pr.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 0, 5000, pr.posting_date],
|
||||
["Provision Account - _TC", 0, 1000, pi.posting_date],
|
||||
["_Test Account Cost for Goods Sold - _TC", 1000, 0, pi.posting_date],
|
||||
]
|
||||
|
||||
check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date)
|
||||
|
||||
toggle_provisional_accounting_setting()
|
||||
|
||||
def test_adjust_incoming_rate(self):
|
||||
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 0)
|
||||
@@ -2274,4 +2352,26 @@ def make_purchase_invoice_against_cost_center(**args):
|
||||
return pi
|
||||
|
||||
|
||||
def setup_provisional_accounting(**args):
|
||||
args = frappe._dict(args)
|
||||
create_item("_Test Non Stock Item", is_stock_item=0)
|
||||
company = args.company or "_Test Company"
|
||||
provisional_account = create_account(
|
||||
account_name=args.account_name or "Provision Account",
|
||||
parent_account=args.parent_account or "Current Liabilities - _TC",
|
||||
company=company,
|
||||
)
|
||||
toggle_provisional_accounting_setting(
|
||||
enable=1, company=company, provisional_account=provisional_account
|
||||
)
|
||||
|
||||
|
||||
def toggle_provisional_accounting_setting(**args):
|
||||
args = frappe._dict(args)
|
||||
company = frappe.get_doc("Company", args.company or "_Test Company")
|
||||
company.enable_provisional_accounting_for_non_stock_items = args.enable or 0
|
||||
company.default_provisional_account = args.provisional_account
|
||||
company.save()
|
||||
|
||||
|
||||
test_records = frappe.get_test_records("Purchase Invoice")
|
||||
|
||||
@@ -3615,6 +3615,33 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
check_gl_entries(self, pe.name, expected_gle, nowdate(), voucher_type="Payment Entry")
|
||||
set_advance_flag(company="_Test Company", flag=0, default_account="")
|
||||
|
||||
def test_pulling_advance_based_on_debit_to(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
|
||||
|
||||
debtors2 = create_account(
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
account_name="Debtors 2",
|
||||
company="_Test Company",
|
||||
account_type="Receivable",
|
||||
)
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
si.debit_to = debtors2
|
||||
si.save()
|
||||
|
||||
pe = create_payment_entry(
|
||||
company=si.company,
|
||||
payment_type="Receive",
|
||||
party_type="Customer",
|
||||
party=si.customer,
|
||||
paid_from=debtors2,
|
||||
paid_to="Cash - _TC",
|
||||
paid_amount=1000,
|
||||
)
|
||||
pe.submit()
|
||||
advances = si.get_advance_entries()
|
||||
self.assertEqual(1, len(advances))
|
||||
self.assertEqual(advances[0].reference_name, pe.name)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<div class="row {% if df.bold %}important{% endif %} data-field">
|
||||
<div class="col-xs-{{ "9" if df.fieldtype=="Check" else "5" }}
|
||||
{%- if doc.align_labels_right %} text-right{%- endif -%}">
|
||||
<label>{{ _(df.label) }}</label>
|
||||
<label>{{ _(df.label, context=df.parent) }}</label>
|
||||
</div>
|
||||
<div class="col-xs-{{ "3" if df.fieldtype=="Check" else "7" }} value">
|
||||
{% if doc.get(df.fieldname) != None -%}
|
||||
|
||||
@@ -1135,21 +1135,24 @@ class AccountsController(TransactionBase):
|
||||
self.append("advances", advance_row)
|
||||
|
||||
def get_advance_entries(self, include_unallocated=True):
|
||||
party_account = []
|
||||
if self.doctype == "Sales Invoice":
|
||||
party_type = "Customer"
|
||||
party = self.customer
|
||||
amount_field = "credit_in_account_currency"
|
||||
order_field = "sales_order"
|
||||
order_doctype = "Sales Order"
|
||||
party_account.append(self.debit_to)
|
||||
else:
|
||||
party_type = "Supplier"
|
||||
party = self.supplier
|
||||
amount_field = "debit_in_account_currency"
|
||||
order_field = "purchase_order"
|
||||
order_doctype = "Purchase Order"
|
||||
party_account.append(self.credit_to)
|
||||
|
||||
party_account = get_party_account(
|
||||
party_type, party=party, company=self.company, include_advance=True
|
||||
party_account.extend(
|
||||
get_party_account(party_type, party=party, company=self.company, include_advance=True)
|
||||
)
|
||||
|
||||
order_list = list(set(d.get(order_field) for d in self.get("items") if d.get(order_field)))
|
||||
|
||||
@@ -35,6 +35,14 @@ frappe.ui.form.on("Project", {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("department", function (doc) {
|
||||
return {
|
||||
filters: {
|
||||
"company": doc.company,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
// sales order
|
||||
frm.set_query('sales_order', function () {
|
||||
var filters = {
|
||||
|
||||
@@ -15,7 +15,7 @@ frappe.ui.form.on('Item Variant Settings', {
|
||||
frappe.model.with_doctype('Item', () => {
|
||||
const field_label_map = {};
|
||||
frappe.get_meta('Item').fields.forEach(d => {
|
||||
field_label_map[d.fieldname] = __(d.label) + ` (${d.fieldname})`;
|
||||
field_label_map[d.fieldname] = __(d.label, null, d.parent) + ` (${d.fieldname})`;
|
||||
|
||||
if (!in_list(exclude_field_types, d.fieldtype)
|
||||
&& !d.no_copy && !in_list(exclude_fields, d.fieldname)) {
|
||||
|
||||
@@ -729,16 +729,19 @@ class PurchaseReceipt(BuyingController):
|
||||
)
|
||||
|
||||
def add_provisional_gl_entry(
|
||||
self, item, gl_entries, posting_date, provisional_account, reverse=0
|
||||
self, item, gl_entries, posting_date, provisional_account, reverse=0, item_amount=None
|
||||
):
|
||||
credit_currency = get_account_currency(provisional_account)
|
||||
expense_account = item.expense_account
|
||||
debit_currency = get_account_currency(item.expense_account)
|
||||
remarks = self.get("remarks") or _("Accounting Entry for Service")
|
||||
multiplication_factor = 1
|
||||
amount = item.base_amount
|
||||
|
||||
if reverse:
|
||||
multiplication_factor = -1
|
||||
# Post reverse entry for previously posted amount
|
||||
amount = item_amount
|
||||
expense_account = frappe.db.get_value(
|
||||
"Purchase Receipt Item", {"name": item.get("pr_detail")}, ["expense_account"]
|
||||
)
|
||||
@@ -748,7 +751,7 @@ class PurchaseReceipt(BuyingController):
|
||||
account=provisional_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=0.0,
|
||||
credit=multiplication_factor * item.base_amount,
|
||||
credit=multiplication_factor * amount,
|
||||
remarks=remarks,
|
||||
against_account=expense_account,
|
||||
account_currency=credit_currency,
|
||||
@@ -762,7 +765,7 @@ class PurchaseReceipt(BuyingController):
|
||||
gl_entries=gl_entries,
|
||||
account=expense_account,
|
||||
cost_center=item.cost_center,
|
||||
debit=multiplication_factor * item.base_amount,
|
||||
debit=multiplication_factor * amount,
|
||||
credit=0.0,
|
||||
remarks=remarks,
|
||||
against_account=provisional_account,
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
if((df.fieldname !== "description" && df.fieldname !== "item_name") && val) { %}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 ellipsis">
|
||||
<strong title="{%= __(df.label) %}">{%= __(df.label) %}:</strong>
|
||||
<strong title="{%= __(df.label, null, df.parent) %}">{%= __(df.label, null, df.parent) %}:</strong>
|
||||
</div>
|
||||
<div class="col-xs-8">
|
||||
{%= doc.get_formatted(df.fieldname) %}
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<p>
|
||||
<a href="/api/method/erpnext.accounts.doctype.payment_request.payment_request.make_payment_request?dn={{ doc.name }}&dt={{ doc.doctype }}&submit_doc=1&order_type=Shopping Cart"
|
||||
class="btn btn-primary btn-sm" id="pay-for-order">
|
||||
{{ _("Pay") }} {{doc.get_formatted("grand_total") }}
|
||||
{{ _("Pay") }} {{ pay_amount }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import get_amount
|
||||
|
||||
|
||||
def get_context(context):
|
||||
context.no_cache = 1
|
||||
@@ -48,10 +50,7 @@ def get_context(context):
|
||||
)
|
||||
context.available_loyalty_points = int(loyalty_program_details.get("loyalty_points"))
|
||||
|
||||
context.show_pay_button = (
|
||||
"payments" in frappe.get_installed_apps()
|
||||
and frappe.db.get_single_value("Buying Settings", "show_pay_button")
|
||||
)
|
||||
context.show_pay_button, context.pay_amount = get_payment_details(context.doc)
|
||||
context.show_make_pi_button = False
|
||||
if context.doc.get("supplier"):
|
||||
# show Make Purchase Invoice button based on permission
|
||||
@@ -64,3 +63,14 @@ def get_attachments(dt, dn):
|
||||
fields=["name", "file_name", "file_url", "is_private"],
|
||||
filters={"attached_to_name": dn, "attached_to_doctype": dt, "is_private": 0},
|
||||
)
|
||||
|
||||
|
||||
def get_payment_details(doc):
|
||||
show_pay_button, amount = (
|
||||
"payments" in frappe.get_installed_apps()
|
||||
and frappe.db.get_single_value("Buying Settings", "show_pay_button")
|
||||
), 0
|
||||
if not show_pay_button:
|
||||
return show_pay_button, amount
|
||||
amount = get_amount(doc)
|
||||
return bool(amount), amount
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
|
||||
<label>{{ _(df.label) }}</label></div>
|
||||
<label>{{ _(df.label, context=df.parent) }}</label></div>
|
||||
<div class="col-xs-7 text-right">
|
||||
{{ doc.get_formatted("total", doc) }}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user