mirror of
https://github.com/frappe/erpnext.git
synced 2026-02-19 09:35:03 +00:00
Merge pull request #44483 from frappe/version-15-hotfix
chore: release v15
This commit is contained in:
@@ -19,16 +19,6 @@
|
||||
"currency",
|
||||
"column_break_11",
|
||||
"conversion_rate",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_6",
|
||||
"dunning_type",
|
||||
"column_break_8",
|
||||
@@ -56,7 +46,21 @@
|
||||
"income_account",
|
||||
"column_break_48",
|
||||
"cost_center",
|
||||
"amended_from"
|
||||
"amended_from",
|
||||
"address_and_contact_tab",
|
||||
"address_and_contact_section",
|
||||
"customer_address",
|
||||
"address_display",
|
||||
"column_break_vodj",
|
||||
"contact_person",
|
||||
"contact_display",
|
||||
"contact_mobile",
|
||||
"contact_email",
|
||||
"section_break_xban",
|
||||
"column_break_16",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"column_break_lqmf"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -178,10 +182,8 @@
|
||||
"label": "Rate of Interest (%) Yearly"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "address_and_contact_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Address and Contact"
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_display",
|
||||
@@ -377,11 +379,28 @@
|
||||
{
|
||||
"fieldname": "column_break_48",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "address_and_contact_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Address & Contact"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_vodj",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_xban",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_lqmf",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-06-15 15:46:53.865712",
|
||||
"modified": "2024-11-26 13:46:07.760867",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dunning",
|
||||
@@ -435,4 +454,4 @@
|
||||
"states": [],
|
||||
"title_field": "customer_name",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
|
||||
pe = get_payment_entry(si.doctype, si.name)
|
||||
pe.paid_amount = 95
|
||||
pe.source_exchange_rate = 84.211
|
||||
pe.source_exchange_rate = 84.2105
|
||||
pe.received_amount = 8000
|
||||
pe.references = []
|
||||
pe.save().submit()
|
||||
@@ -229,7 +229,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, FrappeTestCase):
|
||||
row = next(x for x in je.accounts if x.account == self.debtors_usd)
|
||||
self.assertEqual(flt(row.credit_in_account_currency, precision), 5.0) # in USD
|
||||
row = next(x for x in je.accounts if x.account != self.debtors_usd)
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.06) # in INR
|
||||
self.assertEqual(flt(row.debit_in_account_currency, precision), 421.05) # in INR
|
||||
|
||||
# total_debit and total_credit will be 0.0, as JV is posting only to account currency fields
|
||||
self.assertEqual(flt(je.total_debit, precision), 0.0)
|
||||
|
||||
@@ -127,9 +127,6 @@ class JournalEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
self.set_total_debit_credit()
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
@@ -184,6 +181,11 @@ class JournalEntry(AccountsController):
|
||||
else:
|
||||
return self._cancel()
|
||||
|
||||
def before_submit(self):
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
def on_submit(self):
|
||||
self.validate_cheque_info()
|
||||
self.check_credit_limit()
|
||||
|
||||
@@ -185,6 +185,10 @@ frappe.ui.form.on("Payment Entry", {
|
||||
filters: {
|
||||
reference_doctype: row.reference_doctype,
|
||||
reference_name: row.reference_name,
|
||||
company: doc.company,
|
||||
status: ["!=", "Paid"],
|
||||
outstanding_amount: [">", 0], // for compatibility with old data
|
||||
docstatus: 1,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -320,11 +324,6 @@ frappe.ui.form.on("Payment Entry", {
|
||||
"write_off_difference_amount",
|
||||
frm.doc.difference_amount && frm.doc.party && frm.doc.total_allocated_amount > party_amount
|
||||
);
|
||||
|
||||
frm.toggle_display(
|
||||
"set_exchange_gain_loss",
|
||||
frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount
|
||||
);
|
||||
},
|
||||
|
||||
set_dynamic_labels: function (frm) {
|
||||
@@ -1115,36 +1114,34 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
set_unallocated_amount: function (frm) {
|
||||
var unallocated_amount = 0;
|
||||
var total_deductions = frappe.utils.sum(
|
||||
$.map(frm.doc.deductions || [], function (d) {
|
||||
return flt(d.amount);
|
||||
})
|
||||
);
|
||||
let unallocated_amount = 0;
|
||||
let deductions_to_consider = 0;
|
||||
|
||||
for (const row of frm.doc.deductions || []) {
|
||||
if (!row.is_exchange_gain_loss) deductions_to_consider += flt(row.amount);
|
||||
}
|
||||
const included_taxes = get_included_taxes(frm);
|
||||
|
||||
if (frm.doc.party) {
|
||||
if (
|
||||
frm.doc.payment_type == "Receive" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount + total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.paid_amount + total_deductions / frm.doc.source_exchange_rate
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount +
|
||||
total_deductions -
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
frm.doc.base_total_allocated_amount) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount - total_deductions &&
|
||||
frm.doc.total_allocated_amount <
|
||||
frm.doc.received_amount + total_deductions / frm.doc.target_exchange_rate
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_paid_amount + deductions_to_consider
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_paid_amount +
|
||||
flt(frm.doc.base_total_taxes_and_charges) -
|
||||
(total_deductions + frm.doc.base_total_allocated_amount)) /
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
frm.doc.source_exchange_rate;
|
||||
} else if (
|
||||
frm.doc.payment_type == "Pay" &&
|
||||
frm.doc.base_total_allocated_amount < frm.doc.base_received_amount - deductions_to_consider
|
||||
) {
|
||||
unallocated_amount =
|
||||
(frm.doc.base_received_amount -
|
||||
deductions_to_consider -
|
||||
frm.doc.base_total_allocated_amount -
|
||||
included_taxes) /
|
||||
frm.doc.target_exchange_rate;
|
||||
}
|
||||
}
|
||||
@@ -1238,77 +1235,85 @@ frappe.ui.form.on("Payment Entry", {
|
||||
},
|
||||
|
||||
write_off_difference_amount: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "write_off_account");
|
||||
frm.events.set_write_off_deduction(frm);
|
||||
},
|
||||
|
||||
set_exchange_gain_loss: function (frm) {
|
||||
frm.events.set_deductions_entry(frm, "exchange_gain_loss_account");
|
||||
base_paid_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
set_deductions_entry: function (frm, account) {
|
||||
if (frm.doc.difference_amount) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
callback: function (r, rt) {
|
||||
if (r.message) {
|
||||
const write_off_row = $.map(frm.doc["deductions"] || [], function (t) {
|
||||
return t.account == r.message[account] ? t : null;
|
||||
});
|
||||
base_received_amount: function (frm) {
|
||||
frm.events.set_exchange_gain_loss_deduction(frm);
|
||||
},
|
||||
|
||||
const difference_amount = flt(
|
||||
frm.doc.difference_amount,
|
||||
precision("difference_amount")
|
||||
);
|
||||
set_exchange_gain_loss_deduction: async function (frm) {
|
||||
// wait for allocate_party_amount_against_ref_docs to finish
|
||||
await frappe.after_ajax();
|
||||
const base_paid_amount = frm.doc.base_paid_amount || 0;
|
||||
const base_received_amount = frm.doc.base_received_amount || 0;
|
||||
const exchange_gain_loss = flt(
|
||||
base_paid_amount - base_received_amount,
|
||||
get_deduction_amount_precision()
|
||||
);
|
||||
|
||||
const add_deductions = (details) => {
|
||||
let row = null;
|
||||
if (!write_off_row.length && difference_amount) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = details[account];
|
||||
row.cost_center = details["cost_center"];
|
||||
} else {
|
||||
row = write_off_row[0];
|
||||
}
|
||||
|
||||
if (row) {
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
} else {
|
||||
frappe.msgprint(__("No gain or loss in the exchange rate"));
|
||||
}
|
||||
refresh_field("deductions");
|
||||
};
|
||||
|
||||
if (!r.message[account]) {
|
||||
frappe.prompt(
|
||||
{
|
||||
label: __("Please Specify Account"),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => {
|
||||
const details = Object.assign({}, r.message, values);
|
||||
add_deductions(details);
|
||||
},
|
||||
__(frappe.unscrub(account))
|
||||
);
|
||||
} else {
|
||||
add_deductions(r.message);
|
||||
}
|
||||
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
}
|
||||
},
|
||||
});
|
||||
if (!exchange_gain_loss) {
|
||||
frm.events.delete_exchange_gain_loss(frm);
|
||||
return;
|
||||
}
|
||||
|
||||
const account_fieldname = "exchange_gain_loss_account";
|
||||
let row = (frm.doc.deductions || []).find((t) => t.is_exchange_gain_loss);
|
||||
|
||||
if (!row) {
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
|
||||
const account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
row = frm.add_child("deductions");
|
||||
row.account = account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
row.is_exchange_gain_loss = 1;
|
||||
}
|
||||
|
||||
row.amount = exchange_gain_loss;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
delete_exchange_gain_loss: function (frm) {
|
||||
const exchange_gain_loss_row = (frm.doc.deductions || []).find((row) => row.is_exchange_gain_loss);
|
||||
|
||||
if (!exchange_gain_loss_row) return;
|
||||
|
||||
exchange_gain_loss_row.amount = 0;
|
||||
frm.get_field("deductions").grid.grid_rows[exchange_gain_loss_row.idx - 1].remove();
|
||||
frm.refresh_field("deductions");
|
||||
},
|
||||
|
||||
set_write_off_deduction: async function (frm) {
|
||||
const difference_amount = flt(frm.doc.difference_amount, get_deduction_amount_precision());
|
||||
if (!difference_amount) return;
|
||||
|
||||
const account_fieldname = "write_off_account";
|
||||
const response = await get_company_defaults(frm.doc.company);
|
||||
const write_off_account =
|
||||
response.message?.[account_fieldname] ||
|
||||
(await prompt_for_missing_account(frm, account_fieldname));
|
||||
|
||||
if (!write_off_account) return;
|
||||
|
||||
let row = (frm.doc["deductions"] || []).find((t) => t.account == write_off_account);
|
||||
if (!row) {
|
||||
row = frm.add_child("deductions");
|
||||
row.account = write_off_account;
|
||||
row.cost_center = response.message?.cost_center;
|
||||
}
|
||||
|
||||
row.amount = flt(row.amount) + difference_amount;
|
||||
frm.refresh_field("deductions");
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
|
||||
bank_account: function (frm) {
|
||||
@@ -1774,6 +1779,13 @@ frappe.ui.form.on("Advance Taxes and Charges", {
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Payment Entry Deduction", {
|
||||
before_deductions_remove: function (doc, cdt, cdn) {
|
||||
const row = frappe.get_doc(cdt, cdn);
|
||||
if (row.is_exchange_gain_loss && row.amount) {
|
||||
frappe.throw(__("Cannot delete Exchange Gain/Loss row"));
|
||||
}
|
||||
},
|
||||
|
||||
amount: function (frm) {
|
||||
frm.events.set_unallocated_amount(frm);
|
||||
},
|
||||
@@ -1795,3 +1807,53 @@ function set_default_party_type(frm) {
|
||||
|
||||
if (party_type) frm.set_value("party_type", party_type);
|
||||
}
|
||||
|
||||
function get_included_taxes(frm) {
|
||||
let included_taxes = 0;
|
||||
for (const tax of frm.doc.taxes) {
|
||||
if (!tax.included_in_paid_amount) continue;
|
||||
|
||||
if (tax.add_deduct_tax == "Add") {
|
||||
included_taxes += tax.base_tax_amount;
|
||||
} else {
|
||||
included_taxes -= tax.base_tax_amount;
|
||||
}
|
||||
}
|
||||
|
||||
return included_taxes;
|
||||
}
|
||||
|
||||
function get_company_defaults(company) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_company_defaults",
|
||||
args: {
|
||||
company: company,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function prompt_for_missing_account(frm, account) {
|
||||
return new Promise((resolve) => {
|
||||
const dialog = frappe.prompt(
|
||||
{
|
||||
label: __(frappe.unscrub(account)),
|
||||
fieldname: account,
|
||||
fieldtype: "Link",
|
||||
options: "Account",
|
||||
get_query: () => ({
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
},
|
||||
}),
|
||||
},
|
||||
(values) => resolve(values?.[account]),
|
||||
__("Please Specify Account")
|
||||
);
|
||||
|
||||
dialog.on_hide = () => resolve("");
|
||||
});
|
||||
}
|
||||
|
||||
function get_deduction_amount_precision() {
|
||||
return frappe.meta.get_field_precision(frappe.meta.get_field("Payment Entry Deduction", "amount"));
|
||||
}
|
||||
|
||||
@@ -56,7 +56,6 @@
|
||||
"section_break_34",
|
||||
"total_allocated_amount",
|
||||
"base_total_allocated_amount",
|
||||
"set_exchange_gain_loss",
|
||||
"column_break_36",
|
||||
"unallocated_amount",
|
||||
"difference_amount",
|
||||
@@ -390,11 +389,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "set_exchange_gain_loss",
|
||||
"fieldtype": "Button",
|
||||
"label": "Set Exchange Gain / Loss"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
"fieldtype": "Column Break"
|
||||
@@ -801,7 +795,7 @@
|
||||
"table_fieldname": "payment_entries"
|
||||
}
|
||||
],
|
||||
"modified": "2024-05-31 17:07:06.197249",
|
||||
"modified": "2024-11-07 11:19:19.320883",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
|
||||
@@ -893,6 +893,7 @@ class PaymentEntry(AccountsController):
|
||||
self.set_amounts_in_company_currency()
|
||||
self.set_total_allocated_amount()
|
||||
self.set_unallocated_amount()
|
||||
self.set_exchange_gain_loss()
|
||||
self.set_difference_amount()
|
||||
|
||||
def validate_amounts(self):
|
||||
@@ -988,10 +989,10 @@ class PaymentEntry(AccountsController):
|
||||
if d.exchange_rate is None:
|
||||
d.exchange_rate = 1
|
||||
|
||||
allocated_amount_in_pe_exchange_rate = flt(
|
||||
allocated_amount_in_ref_exchange_rate = flt(
|
||||
flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
|
||||
)
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_pe_exchange_rate
|
||||
d.exchange_gain_loss = base_allocated_amount - allocated_amount_in_ref_exchange_rate
|
||||
return base_allocated_amount
|
||||
|
||||
def set_total_allocated_amount(self):
|
||||
@@ -1009,29 +1010,80 @@ class PaymentEntry(AccountsController):
|
||||
|
||||
def set_unallocated_amount(self):
|
||||
self.unallocated_amount = 0
|
||||
if self.party:
|
||||
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
|
||||
included_taxes = self.get_included_taxes()
|
||||
if (
|
||||
self.payment_type == "Receive"
|
||||
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
|
||||
and self.total_allocated_amount
|
||||
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount + total_deductions - self.base_total_allocated_amount
|
||||
) / self.source_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
elif (
|
||||
self.payment_type == "Pay"
|
||||
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
|
||||
and self.total_allocated_amount
|
||||
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
|
||||
) / self.target_exchange_rate
|
||||
self.unallocated_amount -= included_taxes
|
||||
if not self.party:
|
||||
return
|
||||
|
||||
deductions_to_consider = sum(
|
||||
flt(d.amount) for d in self.get("deductions") if not d.is_exchange_gain_loss
|
||||
)
|
||||
included_taxes = self.get_included_taxes()
|
||||
|
||||
if self.payment_type == "Receive" and self.base_total_allocated_amount < (
|
||||
self.base_paid_amount + deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_paid_amount
|
||||
+ deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.source_exchange_rate
|
||||
elif self.payment_type == "Pay" and self.base_total_allocated_amount < (
|
||||
self.base_received_amount - deductions_to_consider
|
||||
):
|
||||
self.unallocated_amount = (
|
||||
self.base_received_amount
|
||||
- deductions_to_consider
|
||||
- self.base_total_allocated_amount
|
||||
- included_taxes
|
||||
) / self.target_exchange_rate
|
||||
|
||||
def set_exchange_gain_loss(self):
|
||||
exchange_gain_loss = flt(
|
||||
self.base_paid_amount - self.base_received_amount,
|
||||
self.precision("amount", "deductions"),
|
||||
)
|
||||
|
||||
exchange_gain_loss_rows = [row for row in self.get("deductions") if row.is_exchange_gain_loss]
|
||||
exchange_gain_loss_row = exchange_gain_loss_rows.pop(0) if exchange_gain_loss_rows else None
|
||||
|
||||
for row in exchange_gain_loss_rows:
|
||||
self.remove(row)
|
||||
|
||||
if not exchange_gain_loss:
|
||||
if exchange_gain_loss_row:
|
||||
self.remove(exchange_gain_loss_row)
|
||||
|
||||
return
|
||||
|
||||
if not exchange_gain_loss_row:
|
||||
values = frappe.get_cached_value(
|
||||
"Company", self.company, ("exchange_gain_loss_account", "cost_center"), as_dict=True
|
||||
)
|
||||
|
||||
for fieldname, value in values.items():
|
||||
if value:
|
||||
continue
|
||||
|
||||
label = _(frappe.get_meta("Company").get_label(fieldname))
|
||||
return frappe.msgprint(
|
||||
_("Please set {0} in Company {1} to account for Exchange Gain / Loss").format(
|
||||
label, get_link_to_form("Company", self.company)
|
||||
),
|
||||
title=_("Missing Default in Company"),
|
||||
indicator="red" if self.docstatus.is_submitted() else "yellow",
|
||||
raise_exception=self.docstatus.is_submitted(),
|
||||
)
|
||||
|
||||
exchange_gain_loss_row = self.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": values.exchange_gain_loss_account,
|
||||
"cost_center": values.cost_center,
|
||||
"is_exchange_gain_loss": 1,
|
||||
},
|
||||
)
|
||||
|
||||
exchange_gain_loss_row.amount = exchange_gain_loss
|
||||
|
||||
def set_difference_amount(self):
|
||||
base_unallocated_amount = flt(self.unallocated_amount) * (
|
||||
@@ -1059,11 +1111,13 @@ class PaymentEntry(AccountsController):
|
||||
def get_included_taxes(self):
|
||||
included_taxes = 0
|
||||
for tax in self.get("taxes"):
|
||||
if tax.included_in_paid_amount:
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
if not tax.included_in_paid_amount:
|
||||
continue
|
||||
|
||||
if tax.add_deduct_tax == "Add":
|
||||
included_taxes += tax.base_tax_amount
|
||||
else:
|
||||
included_taxes -= tax.base_tax_amount
|
||||
|
||||
return included_taxes
|
||||
|
||||
@@ -1912,8 +1966,8 @@ class PaymentEntry(AccountsController):
|
||||
def get_matched_payment_request_of_references(references=None):
|
||||
"""
|
||||
Get those `Payment Requests` which are matched with `References`.\n
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
- Amount must be same.
|
||||
- Only single `Payment Request` available for this amount.
|
||||
|
||||
Example: [(reference_doctype, reference_name, allocated_amount, payment_request), ...]
|
||||
"""
|
||||
@@ -2015,7 +2069,7 @@ def get_outstanding_of_references_with_payment_term(references=None):
|
||||
def get_outstanding_of_references_with_no_payment_term(references):
|
||||
"""
|
||||
Fetch outstanding amount of `References` which have no `Payment Term` set.\n
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
- Fetch outstanding amount from `References` it self.
|
||||
|
||||
Note: `None` is used for allocation of `Payment Request`
|
||||
Example: {(reference_doctype, reference_name, None): outstanding_amount, ...}
|
||||
@@ -2829,9 +2883,6 @@ def get_payment_entry(
|
||||
update_accounting_dimensions(pe, doc)
|
||||
|
||||
if party_account and bank:
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
if discount_amount:
|
||||
base_total_discount_loss = 0
|
||||
if frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss"):
|
||||
@@ -2841,7 +2892,8 @@ def get_payment_entry(
|
||||
pe, doc, discount_amount, base_total_discount_loss, party_account_currency
|
||||
)
|
||||
|
||||
pe.set_difference_amount()
|
||||
pe.set_exchange_rate(ref_doc=doc)
|
||||
pe.set_amounts()
|
||||
|
||||
# If PE is created from PR directly, then no need to find open PRs for the references
|
||||
if not created_from_payment_request:
|
||||
@@ -2853,7 +2905,7 @@ def get_payment_entry(
|
||||
def get_open_payment_requests_for_references(references=None):
|
||||
"""
|
||||
Fetch all unpaid Payment Requests for the references. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
- Each reference can have multiple Payment Requests. \n
|
||||
|
||||
Example: {("Sales Invoice", "SINV-00001"): {"PREQ-00001": 1000, "PREQ-00002": 2000}}
|
||||
"""
|
||||
@@ -2877,6 +2929,7 @@ def get_open_payment_requests_for_references(references=None):
|
||||
.where(Tuple(PR.reference_doctype, PR.reference_name).isin(list(refs)))
|
||||
.where(PR.status != "Paid")
|
||||
.where(PR.docstatus == 1)
|
||||
.where(PR.outstanding_amount > 0) # to avoid old PRs with 0 outstanding amount
|
||||
.orderby(Coalesce(PR.transaction_date, PR.creation), order=frappe.qb.asc)
|
||||
).run(as_dict=True)
|
||||
|
||||
@@ -3187,13 +3240,14 @@ def set_pending_discount_loss(pe, doc, discount_amount, base_total_discount_loss
|
||||
book_tax_loss = frappe.db.get_single_value("Accounts Settings", "book_tax_discount_loss")
|
||||
account_type = "round_off_account" if book_tax_loss else "default_discount_account"
|
||||
|
||||
pe.set_gain_or_loss(
|
||||
account_details={
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": frappe.get_cached_value("Company", pe.company, account_type),
|
||||
"cost_center": pe.cost_center
|
||||
or frappe.get_cached_value("Company", pe.company, "cost_center"),
|
||||
"amount": discount_amount * positive_negative,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -479,16 +479,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
self.assertEqual(pe.deductions[0].account, "Write Off - _TC")
|
||||
|
||||
# Exchange loss
|
||||
self.assertEqual(pe.difference_amount, 300.0)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 300.0,
|
||||
},
|
||||
)
|
||||
self.assertEqual(pe.deductions[-1].amount, 300.0)
|
||||
pe.deductions[-1].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[-1].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
@@ -552,16 +545,10 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = "2016-01-01"
|
||||
|
||||
self.assertEqual(pe.difference_amount, 100)
|
||||
self.assertEqual(pe.deductions[0].amount, 100)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 100,
|
||||
},
|
||||
)
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -654,16 +641,9 @@ class TestPaymentEntry(FrappeTestCase):
|
||||
pe.set_exchange_rate()
|
||||
pe.set_amounts()
|
||||
|
||||
self.assertEqual(pe.difference_amount, 500)
|
||||
|
||||
pe.append(
|
||||
"deductions",
|
||||
{
|
||||
"account": "_Test Exchange Gain/Loss - _TC",
|
||||
"cost_center": "_Test Cost Center - _TC",
|
||||
"amount": 500,
|
||||
},
|
||||
)
|
||||
self.assertEqual(pe.deductions[0].amount, 500)
|
||||
pe.deductions[0].account = "_Test Exchange Gain/Loss - _TC"
|
||||
pe.deductions[0].cost_center = "_Test Cost Center - _TC"
|
||||
|
||||
pe.insert()
|
||||
pe.submit()
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
"cost_center",
|
||||
"amount",
|
||||
"column_break_2",
|
||||
"is_exchange_gain_loss",
|
||||
"description"
|
||||
],
|
||||
"fields": [
|
||||
@@ -45,12 +46,20 @@
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"label": "Description"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:doc.is_exchange_gain_loss",
|
||||
"fieldname": "is_exchange_gain_loss",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Exchange Gain / Loss?",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2023-03-06 07:11:57.739619",
|
||||
"modified": "2024-11-05 16:07:47.307971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Deduction",
|
||||
|
||||
@@ -18,6 +18,7 @@ class PaymentEntryDeduction(Document):
|
||||
amount: DF.Currency
|
||||
cost_center: DF.Link
|
||||
description: DF.SmallText | None
|
||||
is_exchange_gain_loss: DF.Check
|
||||
parent: DF.Data
|
||||
parentfield: DF.Data
|
||||
parenttype: DF.Data
|
||||
|
||||
@@ -778,6 +778,8 @@ def get_existing_paid_amount(doctype, name):
|
||||
.where(PL.against_voucher_type.eq(doctype))
|
||||
.where(PL.against_voucher_no.eq(name))
|
||||
.where(PL.amount < 0)
|
||||
.where(PL.delinked == 0)
|
||||
.where(PER.docstatus == 1)
|
||||
.where(PER.payment_request.isnull())
|
||||
)
|
||||
response = query.run()
|
||||
@@ -987,12 +989,7 @@ def get_open_payment_requests_query(doctype, txt, searchfield, start, page_len,
|
||||
|
||||
open_payment_requests = frappe.get_list(
|
||||
"Payment Request",
|
||||
filters={
|
||||
**filters,
|
||||
"status": ["!=", "Paid"],
|
||||
"outstanding_amount": ["!=", 0], # for compatibility with old data
|
||||
"docstatus": 1,
|
||||
},
|
||||
filters=filters,
|
||||
fields=["name", "grand_total", "outstanding_amount"],
|
||||
order_by="transaction_date ASC,creation ASC",
|
||||
)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
from frappe import _
|
||||
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
"fieldname": "payment_request",
|
||||
"internal_links": {
|
||||
"Payment Entry": ["references", "payment_request"],
|
||||
"Payment Order": ["references", "payment_order"],
|
||||
},
|
||||
"transactions": [
|
||||
{"label": _("Payment"), "items": ["Payment Entry", "Payment Order"]},
|
||||
],
|
||||
}
|
||||
@@ -1,19 +1,18 @@
|
||||
const INDICATORS = {
|
||||
"Partially Paid": "orange",
|
||||
Cancelled: "red",
|
||||
Draft: "gray",
|
||||
Failed: "red",
|
||||
Initiated: "green",
|
||||
Paid: "blue",
|
||||
Requested: "green",
|
||||
};
|
||||
|
||||
frappe.listview_settings["Payment Request"] = {
|
||||
add_fields: ["status"],
|
||||
get_indicator: function (doc) {
|
||||
if (doc.status == "Draft") {
|
||||
return [__("Draft"), "gray", "status,=,Draft"];
|
||||
}
|
||||
if (doc.status == "Requested") {
|
||||
return [__("Requested"), "green", "status,=,Requested"];
|
||||
} else if (doc.status == "Initiated") {
|
||||
return [__("Initiated"), "green", "status,=,Initiated"];
|
||||
} else if (doc.status == "Partially Paid") {
|
||||
return [__("Partially Paid"), "orange", "status,=,Partially Paid"];
|
||||
} else if (doc.status == "Paid") {
|
||||
return [__("Paid"), "blue", "status,=,Paid"];
|
||||
} else if (doc.status == "Cancelled") {
|
||||
return [__("Cancelled"), "red", "status,=,Cancelled"];
|
||||
}
|
||||
if (!doc.status || !INDICATORS[doc.status]) return;
|
||||
|
||||
return [__(doc.status), INDICATORS[doc.status], `status,=,${doc.status}`];
|
||||
},
|
||||
};
|
||||
|
||||
@@ -543,3 +543,30 @@ class TestPaymentRequest(FrappeTestCase):
|
||||
pr = make_payment_request(dt="Sales Invoice", dn=si.name, mute_email=1)
|
||||
|
||||
self.assertEqual(pr.grand_total, si.outstanding_amount)
|
||||
|
||||
|
||||
def test_partial_paid_invoice_with_submitted_payment_entry(self):
|
||||
pi = make_purchase_invoice(currency="INR", qty=1, rate=5000)
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0001"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
pe.cancel()
|
||||
|
||||
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PURINV0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
pi.load_from_db()
|
||||
pr = make_payment_request(dt="Purchase Invoice", dn=pi.name, mute_email=1)
|
||||
self.assertEqual(pr.grand_total, pi.outstanding_amount)
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -1558,12 +1559,19 @@
|
||||
"fieldname": "update_billed_amount_in_delivery_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Update Billed Amount in Delivery Note"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 16:00:34.268756",
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
|
||||
@@ -32,12 +32,8 @@ class POSInvoice(SalesInvoice):
|
||||
from erpnext.accounts.doctype.payment_schedule.payment_schedule import PaymentSchedule
|
||||
from erpnext.accounts.doctype.pos_invoice_item.pos_invoice_item import POSInvoiceItem
|
||||
from erpnext.accounts.doctype.pricing_rule_detail.pricing_rule_detail import PricingRuleDetail
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import (
|
||||
SalesInvoiceAdvance,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import (
|
||||
SalesInvoicePayment,
|
||||
)
|
||||
from erpnext.accounts.doctype.sales_invoice_advance.sales_invoice_advance import SalesInvoiceAdvance
|
||||
from erpnext.accounts.doctype.sales_invoice_payment.sales_invoice_payment import SalesInvoicePayment
|
||||
from erpnext.accounts.doctype.sales_invoice_timesheet.sales_invoice_timesheet import (
|
||||
SalesInvoiceTimesheet,
|
||||
)
|
||||
@@ -75,6 +71,7 @@ class POSInvoice(SalesInvoice):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
|
||||
@@ -1735,6 +1735,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "set_landed_cost_based_on_purchase_invoice_rate", 1)
|
||||
|
||||
# Cost of Item is zero in Purchase Receipt
|
||||
pr = make_purchase_receipt(qty=1, rate=0)
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 0)
|
||||
|
||||
pi = create_purchase_invoice_from_receipt(pr.name)
|
||||
for row in pi.items:
|
||||
row.rate = 150
|
||||
|
||||
pi.save()
|
||||
pi.submit()
|
||||
|
||||
stock_value_difference = frappe.db.get_value(
|
||||
"Stock Ledger Entry",
|
||||
{"voucher_type": "Purchase Receipt", "voucher_no": pr.name},
|
||||
"stock_value_difference",
|
||||
)
|
||||
self.assertEqual(stock_value_difference, 150)
|
||||
|
||||
# Increase the cost of the item
|
||||
|
||||
pr = make_purchase_receipt(qty=1, rate=100)
|
||||
|
||||
@@ -159,8 +159,9 @@
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"company_addr_col_break",
|
||||
"company_address_display",
|
||||
"company_addr_col_break",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"ignore_default_payment_terms_template",
|
||||
@@ -2166,6 +2167,13 @@
|
||||
"label": "Update Outstanding for Self",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2178,7 +2186,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2024-07-18 15:30:39.428519",
|
||||
"modified": "2024-11-26 12:34:09.110690",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2233,4 +2241,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -96,6 +96,7 @@ class SalesInvoice(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
company_tax_id: DF.Data | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
@@ -1754,6 +1755,9 @@ class SalesInvoice(SellingController):
|
||||
|
||||
def update_project(self):
|
||||
unique_projects = list(set([d.project for d in self.get("items") if d.project]))
|
||||
if self.project and self.project not in unique_projects:
|
||||
unique_projects.append(self.project)
|
||||
|
||||
for p in unique_projects:
|
||||
project = frappe.get_doc("Project", p)
|
||||
project.update_billed_amount()
|
||||
|
||||
@@ -4135,6 +4135,102 @@ class TestSalesInvoice(FrappeTestCase):
|
||||
self.assertEqual(len(actual), 4)
|
||||
self.assertEqual(expected, actual)
|
||||
|
||||
@change_settings("Accounts Settings", {"enable_common_party_accounting": True})
|
||||
def test_common_party_with_different_currency_in_debtor_and_creditor(self):
|
||||
from erpnext.accounts.doctype.account.test_account import create_account
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
)
|
||||
from erpnext.accounts.doctype.party_link.party_link import create_party_link
|
||||
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
creditors = create_account(
|
||||
account_name="Creditors INR",
|
||||
parent_account="Accounts Payable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="INR",
|
||||
account_type="Payable",
|
||||
)
|
||||
debtors = create_account(
|
||||
account_name="Debtors USD",
|
||||
parent_account="Accounts Receivable - _TC",
|
||||
company="_Test Company",
|
||||
account_currency="USD",
|
||||
account_type="Receivable",
|
||||
)
|
||||
|
||||
# create a customer
|
||||
customer = make_customer(customer="_Test Common Party USD")
|
||||
cust_doc = frappe.get_doc("Customer", customer)
|
||||
cust_doc.default_currency = "USD"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": debtors,
|
||||
}
|
||||
cust_doc.append("accounts", test_account_details)
|
||||
cust_doc.save()
|
||||
|
||||
# create a supplier
|
||||
supplier = create_supplier(supplier_name="_Test Common Party INR").name
|
||||
supp_doc = frappe.get_doc("Supplier", supplier)
|
||||
supp_doc.default_currency = "INR"
|
||||
test_account_details = {
|
||||
"company": "_Test Company",
|
||||
"account": creditors,
|
||||
}
|
||||
supp_doc.append("accounts", test_account_details)
|
||||
supp_doc.save()
|
||||
|
||||
# create a party link between customer & supplier
|
||||
create_party_link("Supplier", supplier, customer)
|
||||
|
||||
# create a sales invoice
|
||||
si = create_sales_invoice(
|
||||
customer=customer,
|
||||
currency="USD",
|
||||
conversion_rate=get_exchange_rate("USD", "INR"),
|
||||
debit_to=debtors,
|
||||
do_not_save=1,
|
||||
)
|
||||
si.party_account_currency = "USD"
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
# check outstanding of sales invoice
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
self.assertEqual(flt(si.outstanding_amount), 0.0)
|
||||
|
||||
# check creation of journal entry
|
||||
jv = frappe.get_all(
|
||||
"Journal Entry Account",
|
||||
{
|
||||
"account": si.debit_to,
|
||||
"party_type": "Customer",
|
||||
"party": si.customer,
|
||||
"reference_type": si.doctype,
|
||||
"reference_name": si.name,
|
||||
},
|
||||
pluck="credit_in_account_currency",
|
||||
)
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
def test_total_billed_amount(self):
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
|
||||
project = frappe.new_doc("Project")
|
||||
project.project_name = "Test Total Billed Amount"
|
||||
project.save()
|
||||
|
||||
si.project = project.name
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
doc = frappe.get_doc("Project", project.name)
|
||||
self.assertEqual(doc.total_billed_amount, si.grand_total)
|
||||
|
||||
|
||||
def set_advance_flag(company, flag, default_account):
|
||||
frappe.db.set_value(
|
||||
|
||||
@@ -262,6 +262,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
pe1.paid_from = self.debtors_usd
|
||||
pe1.paid_from_account_currency = "USD"
|
||||
pe1.source_exchange_rate = 75
|
||||
pe1.paid_amount = 100
|
||||
pe1.received_amount = 75 * 100
|
||||
pe1.save()
|
||||
# Allocate payment against both invoices
|
||||
@@ -279,6 +280,7 @@ class TestUnreconcilePayment(AccountsTestMixin, FrappeTestCase):
|
||||
pe2.paid_from = self.debtors_usd
|
||||
pe2.paid_from_account_currency = "USD"
|
||||
pe2.source_exchange_rate = 75
|
||||
pe2.paid_amount = 100
|
||||
pe2.received_amount = 75 * 100
|
||||
pe2.save()
|
||||
# Allocate payment against both invoices
|
||||
|
||||
@@ -29,6 +29,12 @@ from erpnext.accounts.utils import get_fiscal_year
|
||||
from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen
|
||||
from erpnext.utilities.regional import temporary_flag
|
||||
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render_address
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render_address
|
||||
|
||||
PURCHASE_TRANSACTION_TYPES = {
|
||||
"Supplier Quotation",
|
||||
"Purchase Order",
|
||||
@@ -982,10 +988,4 @@ def add_party_account(party_type, party, company, account):
|
||||
|
||||
|
||||
def render_address(address, check_permissions=True):
|
||||
try:
|
||||
from frappe.contacts.doctype.address.address import render_address as _render
|
||||
except ImportError:
|
||||
# Older frappe versions where this function is not available
|
||||
from frappe.contacts.doctype.address.address import get_address_display as _render
|
||||
|
||||
return frappe.call(_render, address, check_permissions=check_permissions)
|
||||
return frappe.call(_render_address, address, check_permissions=check_permissions)
|
||||
|
||||
@@ -1013,7 +1013,7 @@ class ReceivablePayableReport:
|
||||
|
||||
def get_columns(self):
|
||||
self.columns = []
|
||||
self.add_column(_("Posting Date"), fieldtype="Date")
|
||||
self.add_column(_("Posting Date"), fieldname="posting_date", fieldtype="Date")
|
||||
self.add_column(
|
||||
label=_("Party Type"),
|
||||
fieldname="party_type",
|
||||
@@ -1027,8 +1027,15 @@ class ReceivablePayableReport:
|
||||
options="party_type",
|
||||
width=180,
|
||||
)
|
||||
if self.account_type == "Receivable":
|
||||
label = _("Receivable Account")
|
||||
elif self.account_type == "Payable":
|
||||
label = _("Payable Account")
|
||||
else:
|
||||
label = _("Party Account")
|
||||
|
||||
self.add_column(
|
||||
label=self.account_type + " Account",
|
||||
label=label,
|
||||
fieldname="party_account",
|
||||
fieldtype="Link",
|
||||
options="Account",
|
||||
@@ -1066,7 +1073,7 @@ class ReceivablePayableReport:
|
||||
width=180,
|
||||
)
|
||||
|
||||
self.add_column(label=_("Due Date"), fieldtype="Date")
|
||||
self.add_column(label=_("Due Date"), fieldname="due_date", fieldtype="Date")
|
||||
|
||||
if self.account_type == "Payable":
|
||||
self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data")
|
||||
|
||||
@@ -89,7 +89,9 @@ def get_data(filters):
|
||||
& (DepreciationSchedule.schedule_date == d.posting_date)
|
||||
)
|
||||
).run(as_dict=True)
|
||||
asset_data.accumulated_depreciation_amount = query[0]["accumulated_depreciation_amount"]
|
||||
asset_data.accumulated_depreciation_amount = (
|
||||
query[0]["accumulated_depreciation_amount"] if query else 0
|
||||
)
|
||||
|
||||
else:
|
||||
asset_data.accumulated_depreciation_amount += d.debit
|
||||
|
||||
@@ -7,6 +7,7 @@ from frappe import _
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -101,6 +102,9 @@ def execute(filters=None):
|
||||
period_list, asset, liability, equity, provisional_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
return columns, data, message, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
// Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.query_reports["Cash Flow"] = $.extend({}, erpnext.financial_statements);
|
||||
frappe.query_reports["Cash Flow"] = $.extend(erpnext.financial_statements, {
|
||||
name_field: "section",
|
||||
parent_field: "parent_section",
|
||||
});
|
||||
|
||||
erpnext.utils.add_dimensions("Cash Flow", 10);
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ def execute(filters=None):
|
||||
company=filters.company,
|
||||
)
|
||||
|
||||
cash_flow_accounts = get_cash_flow_accounts()
|
||||
cash_flow_sections = get_cash_flow_accounts()
|
||||
|
||||
# compute net profit / loss
|
||||
income = get_data(
|
||||
@@ -60,14 +60,14 @@ def execute(filters=None):
|
||||
summary_data = {}
|
||||
company_currency = frappe.get_cached_value("Company", filters.company, "default_currency")
|
||||
|
||||
for cash_flow_account in cash_flow_accounts:
|
||||
for cash_flow_section in cash_flow_sections:
|
||||
section_data = []
|
||||
data.append(
|
||||
{
|
||||
"account_name": cash_flow_account["section_header"],
|
||||
"parent_account": None,
|
||||
"section_name": "'" + cash_flow_section["section_header"] + "'",
|
||||
"parent_section": None,
|
||||
"indent": 0.0,
|
||||
"account": cash_flow_account["section_header"],
|
||||
"section": cash_flow_section["section_header"],
|
||||
}
|
||||
)
|
||||
|
||||
@@ -75,31 +75,40 @@ def execute(filters=None):
|
||||
# add first net income in operations section
|
||||
if net_profit_loss:
|
||||
net_profit_loss.update(
|
||||
{"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]}
|
||||
{"indent": 1, "parent_section": cash_flow_sections[0]["section_header"]}
|
||||
)
|
||||
data.append(net_profit_loss)
|
||||
section_data.append(net_profit_loss)
|
||||
|
||||
for account in cash_flow_account["account_types"]:
|
||||
account_data = get_account_type_based_data(
|
||||
filters.company, account["account_type"], period_list, filters.accumulated_values, filters
|
||||
for row in cash_flow_section["account_types"]:
|
||||
row_data = get_account_type_based_data(
|
||||
filters.company, row["account_type"], period_list, filters.accumulated_values, filters
|
||||
)
|
||||
account_data.update(
|
||||
accounts = frappe.get_all(
|
||||
"Account",
|
||||
filters={
|
||||
"account_type": row["account_type"],
|
||||
"is_group": 0,
|
||||
},
|
||||
pluck="name",
|
||||
)
|
||||
row_data.update(
|
||||
{
|
||||
"account_name": account["label"],
|
||||
"account": account["label"],
|
||||
"section_name": row["label"],
|
||||
"section": row["label"],
|
||||
"indent": 1,
|
||||
"parent_account": cash_flow_account["section_header"],
|
||||
"accounts": accounts,
|
||||
"parent_section": cash_flow_section["section_header"],
|
||||
"currency": company_currency,
|
||||
}
|
||||
)
|
||||
data.append(account_data)
|
||||
section_data.append(account_data)
|
||||
data.append(row_data)
|
||||
section_data.append(row_data)
|
||||
|
||||
add_total_row_account(
|
||||
data,
|
||||
section_data,
|
||||
cash_flow_account["section_footer"],
|
||||
cash_flow_section["section_footer"],
|
||||
period_list,
|
||||
company_currency,
|
||||
summary_data,
|
||||
@@ -109,7 +118,7 @@ def execute(filters=None):
|
||||
add_total_row_account(
|
||||
data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters
|
||||
)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company)
|
||||
columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company, True)
|
||||
|
||||
chart = get_chart_data(columns, data, company_currency)
|
||||
|
||||
@@ -217,8 +226,8 @@ def get_start_date(period, accumulated_values, company):
|
||||
|
||||
def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False):
|
||||
total_row = {
|
||||
"account_name": "'" + _("{0}").format(label) + "'",
|
||||
"account": "'" + _("{0}").format(label) + "'",
|
||||
"section_name": "'" + _("{0}").format(label) + "'",
|
||||
"section": "'" + _("{0}").format(label) + "'",
|
||||
"currency": currency,
|
||||
}
|
||||
|
||||
@@ -229,7 +238,7 @@ def add_total_row_account(out, data, label, period_list, currency, summary_data,
|
||||
period_list = get_filtered_list_for_consolidated_report(filters, period_list)
|
||||
|
||||
for row in data:
|
||||
if row.get("parent_account"):
|
||||
if row.get("parent_section"):
|
||||
for period in period_list:
|
||||
key = period if consolidated else period["key"]
|
||||
total_row.setdefault(key, 0.0)
|
||||
@@ -254,13 +263,14 @@ def get_report_summary(summary_data, currency):
|
||||
|
||||
def get_chart_data(columns, data, currency):
|
||||
labels = [d.get("label") for d in columns[2:]]
|
||||
print(data)
|
||||
datasets = [
|
||||
{
|
||||
"name": account.get("account").replace("'", ""),
|
||||
"values": [account.get(d.get("fieldname")) for d in columns[2:]],
|
||||
"name": section.get("section").replace("'", ""),
|
||||
"values": [section.get(d.get("fieldname")) for d in columns[2:]],
|
||||
}
|
||||
for account in data
|
||||
if account.get("parent_account") is None and account.get("currency")
|
||||
for section in data
|
||||
if section.get("parent_section") is None and section.get("currency")
|
||||
]
|
||||
datasets = datasets[:-1]
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
@@ -334,8 +335,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False
|
||||
|
||||
def add_total_row(out, root_type, balance_must_be, period_list, company_currency):
|
||||
total_row = {
|
||||
"account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)),
|
||||
"account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'",
|
||||
"account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'",
|
||||
"currency": company_currency,
|
||||
"opening_balance": 0.0,
|
||||
}
|
||||
@@ -616,11 +617,11 @@ def get_cost_centers_with_children(cost_centers):
|
||||
return list(set(all_cost_centers))
|
||||
|
||||
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None):
|
||||
def get_columns(periodicity, period_list, accumulated_values=1, company=None, cash_flow=False):
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "account",
|
||||
"label": _("Account"),
|
||||
"label": _("Account") if not cash_flow else _("Section"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 300,
|
||||
@@ -668,3 +669,67 @@ def get_filtered_list_for_consolidated_report(filters, period_list):
|
||||
filtered_summary_list.append(period)
|
||||
|
||||
return filtered_summary_list
|
||||
|
||||
|
||||
def compute_growth_view_data(data, columns):
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
for column_idx in range(1, len(columns)):
|
||||
previous_period_key = columns[column_idx - 1].get("key")
|
||||
current_period_key = columns[column_idx].get("key")
|
||||
current_period_value = data_copy[row_idx].get(current_period_key)
|
||||
previous_period_value = data_copy[row_idx].get(previous_period_key)
|
||||
annual_growth = 0
|
||||
|
||||
if current_period_value is None:
|
||||
data[row_idx][current_period_key] = None
|
||||
continue
|
||||
|
||||
if previous_period_value == 0 and current_period_value > 0:
|
||||
annual_growth = 1
|
||||
|
||||
elif previous_period_value > 0:
|
||||
annual_growth = (current_period_value - previous_period_value) / previous_period_value
|
||||
|
||||
growth_percent = round(annual_growth * 100, 2)
|
||||
|
||||
data[row_idx][current_period_key] = growth_percent
|
||||
|
||||
|
||||
def compute_margin_view_data(data, columns, accumulated_values):
|
||||
if not columns:
|
||||
return
|
||||
|
||||
if not accumulated_values:
|
||||
columns.append({"key": "total"})
|
||||
|
||||
data_copy = copy.deepcopy(data)
|
||||
|
||||
base_row = None
|
||||
for row in data_copy:
|
||||
if row.get("account_name") == _("Income"):
|
||||
base_row = row
|
||||
break
|
||||
|
||||
if not base_row:
|
||||
return
|
||||
|
||||
for row_idx in range(len(data_copy)):
|
||||
# Taking the total income from each column (for all the financial years) as the base (100%)
|
||||
row = data_copy[row_idx]
|
||||
if not row:
|
||||
continue
|
||||
|
||||
for column in columns:
|
||||
curr_period = column.get("key")
|
||||
base_value = base_row[curr_period]
|
||||
curr_value = row[curr_period]
|
||||
|
||||
if curr_value is None or base_value <= 0:
|
||||
data[row_idx][curr_period] = None
|
||||
continue
|
||||
|
||||
margin_percent = round((curr_value / base_value) * 100, 2)
|
||||
|
||||
data[row_idx][curr_period] = margin_percent
|
||||
|
||||
@@ -421,10 +421,10 @@ class GrossProfitGenerator:
|
||||
self.load_invoice_items()
|
||||
self.get_delivery_notes()
|
||||
|
||||
self.load_product_bundle()
|
||||
if filters.group_by == "Invoice":
|
||||
self.group_items_by_invoice()
|
||||
|
||||
self.load_product_bundle()
|
||||
self.load_non_stock_items()
|
||||
self.get_returned_invoice_items()
|
||||
self.process()
|
||||
@@ -636,6 +636,7 @@ class GrossProfitGenerator:
|
||||
if packed_item.get("parent_detail_docname") == row.item_row:
|
||||
packed_item_row = row.copy()
|
||||
packed_item_row.warehouse = packed_item.warehouse
|
||||
packed_item_row.qty = packed_item.total_qty * -1
|
||||
buying_amount += self.get_buying_amount(packed_item_row, packed_item.item_code)
|
||||
|
||||
return flt(buying_amount, self.currency_precision)
|
||||
@@ -668,7 +669,9 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
my_sle = self.get_stock_ledger_entries(item_code, row.warehouse)
|
||||
if (row.update_stock or row.dn_detail) and my_sle:
|
||||
parenttype, parent = row.parenttype, row.parent
|
||||
parenttype = row.parenttype
|
||||
parent = row.invoice or row.parent
|
||||
|
||||
if row.dn_detail:
|
||||
parenttype, parent = "Delivery Note", row.delivery_note
|
||||
|
||||
@@ -851,6 +854,7 @@ class GrossProfitGenerator:
|
||||
`tabSales Invoice`.project, `tabSales Invoice`.update_stock,
|
||||
`tabSales Invoice`.customer, `tabSales Invoice`.customer_group,
|
||||
`tabSales Invoice`.territory, `tabSales Invoice Item`.item_code,
|
||||
`tabSales Invoice`.base_net_total as "invoice_base_net_total",
|
||||
`tabSales Invoice Item`.item_name, `tabSales Invoice Item`.description,
|
||||
`tabSales Invoice Item`.warehouse, `tabSales Invoice Item`.item_group,
|
||||
`tabSales Invoice Item`.brand, `tabSales Invoice Item`.so_detail,
|
||||
@@ -911,6 +915,7 @@ class GrossProfitGenerator:
|
||||
"""
|
||||
|
||||
grouped = OrderedDict()
|
||||
product_bundles = self.product_bundles.get("Sales Invoice", {})
|
||||
|
||||
for row in self.si_list:
|
||||
# initialize list with a header row for each new parent
|
||||
@@ -921,8 +926,7 @@ class GrossProfitGenerator:
|
||||
)
|
||||
|
||||
# if item is a bundle, add it's components as seperate rows
|
||||
if frappe.db.exists("Product Bundle", row.item_code):
|
||||
bundled_items = self.get_bundle_items(row)
|
||||
if bundled_items := product_bundles.get(row.parent, {}).get(row.item_code):
|
||||
for x in bundled_items:
|
||||
bundle_item = self.get_bundle_item_row(row, x)
|
||||
grouped.get(row.parent).append(bundle_item)
|
||||
@@ -958,47 +962,40 @@ class GrossProfitGenerator:
|
||||
"item_row": None,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"),
|
||||
"base_net_amount": row.invoice_base_net_total,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_items(self, product_bundle):
|
||||
return frappe.get_all(
|
||||
"Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"]
|
||||
)
|
||||
|
||||
def get_bundle_item_row(self, product_bundle, item):
|
||||
item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code)
|
||||
|
||||
def get_bundle_item_row(self, row, item):
|
||||
return frappe._dict(
|
||||
{
|
||||
"parent_invoice": product_bundle.item_code,
|
||||
"indent": product_bundle.indent + 1,
|
||||
"parent_invoice": row.item_code,
|
||||
"parenttype": row.parenttype,
|
||||
"indent": row.indent + 1,
|
||||
"parent": None,
|
||||
"invoice_or_item": item.item_code,
|
||||
"posting_date": product_bundle.posting_date,
|
||||
"posting_time": product_bundle.posting_time,
|
||||
"project": product_bundle.project,
|
||||
"customer": product_bundle.customer,
|
||||
"customer_group": product_bundle.customer_group,
|
||||
"posting_date": row.posting_date,
|
||||
"posting_time": row.posting_time,
|
||||
"project": row.project,
|
||||
"customer": row.customer,
|
||||
"customer_group": row.customer_group,
|
||||
"item_code": item.item_code,
|
||||
"item_name": item_name,
|
||||
"description": description,
|
||||
"warehouse": product_bundle.warehouse,
|
||||
"item_group": item_group,
|
||||
"brand": brand,
|
||||
"dn_detail": product_bundle.dn_detail,
|
||||
"delivery_note": product_bundle.delivery_note,
|
||||
"qty": (flt(product_bundle.qty) * flt(item.qty)),
|
||||
"item_row": None,
|
||||
"is_return": product_bundle.is_return,
|
||||
"cost_center": product_bundle.cost_center,
|
||||
"item_name": item.item_name,
|
||||
"description": item.description,
|
||||
"warehouse": item.warehouse or row.warehouse,
|
||||
"update_stock": row.update_stock,
|
||||
"item_group": "",
|
||||
"brand": "",
|
||||
"dn_detail": row.dn_detail,
|
||||
"delivery_note": row.delivery_note,
|
||||
"qty": item.total_qty * -1,
|
||||
"item_row": row.item_row,
|
||||
"is_return": row.is_return,
|
||||
"cost_center": row.cost_center,
|
||||
"invoice": row.parent,
|
||||
}
|
||||
)
|
||||
|
||||
def get_bundle_item_details(self, item_code):
|
||||
return frappe.db.get_value("Item", item_code, ["item_name", "description", "item_group", "brand"])
|
||||
|
||||
def get_stock_ledger_entries(self, item_code, warehouse):
|
||||
if item_code and warehouse:
|
||||
if (item_code, warehouse) not in self.sle:
|
||||
|
||||
@@ -7,6 +7,8 @@ from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.report.financial_statements import (
|
||||
compute_growth_view_data,
|
||||
compute_margin_view_data,
|
||||
get_columns,
|
||||
get_data,
|
||||
get_filtered_list_for_consolidated_report,
|
||||
@@ -68,6 +70,12 @@ def execute(filters=None):
|
||||
period_list, filters.periodicity, income, expense, net_profit_loss, currency, filters
|
||||
)
|
||||
|
||||
if filters.get("selected_view") == "Growth":
|
||||
compute_growth_view_data(data, period_list)
|
||||
|
||||
if filters.get("selected_view") == "Margin":
|
||||
compute_margin_view_data(data, period_list, filters.accumulated_values)
|
||||
|
||||
return columns, data, None, chart, report_summary, primitive_summary
|
||||
|
||||
|
||||
|
||||
@@ -92,14 +92,14 @@ class TestUtils(unittest.TestCase):
|
||||
payment_entry.deductions = []
|
||||
payment_entry.save()
|
||||
|
||||
# below is the difference between base_received_amount and base_paid_amount
|
||||
self.assertEqual(payment_entry.difference_amount, -4855.0)
|
||||
# below is the difference between base_paid_amount and base_received_amount (exchange gain)
|
||||
self.assertEqual(payment_entry.deductions[0].amount, -4855.0)
|
||||
|
||||
payment_entry.target_exchange_rate = 62.9
|
||||
payment_entry.save()
|
||||
|
||||
# below is due to change in exchange rate
|
||||
self.assertEqual(payment_entry.references[0].exchange_gain_loss, -4855.0)
|
||||
# after changing the exchange rate, there is no exchange gain / loss
|
||||
self.assertEqual(payment_entry.deductions, [])
|
||||
|
||||
payment_entry.references = []
|
||||
self.assertEqual(payment_entry.difference_amount, 0.0)
|
||||
|
||||
@@ -2465,6 +2465,12 @@ class AccountsController(TransactionBase):
|
||||
secondary_account = get_party_account(secondary_party_type, secondary_party, self.company)
|
||||
primary_account_currency = get_account_currency(primary_account)
|
||||
secondary_account_currency = get_account_currency(secondary_account)
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
|
||||
# Determine if multi-currency journal entry is needed
|
||||
multi_currency = (
|
||||
primary_account_currency != default_currency or secondary_account_currency != default_currency
|
||||
)
|
||||
|
||||
jv = frappe.new_doc("Journal Entry")
|
||||
jv.voucher_type = "Journal Entry"
|
||||
@@ -2489,7 +2495,7 @@ class AccountsController(TransactionBase):
|
||||
advance_entry.cost_center = self.cost_center or erpnext.get_default_cost_center(self.company)
|
||||
advance_entry.is_advance = "Yes"
|
||||
|
||||
# update dimesions
|
||||
# Update dimensions
|
||||
dimensions_dict = frappe._dict()
|
||||
active_dimensions = get_dimensions()[0]
|
||||
for dim in active_dimensions:
|
||||
@@ -2498,17 +2504,58 @@ class AccountsController(TransactionBase):
|
||||
reconcilation_entry.update(dimensions_dict)
|
||||
advance_entry.update(dimensions_dict)
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
# Calculate exchange rates if necessary
|
||||
if multi_currency:
|
||||
# Exchange rates for primary and secondary accounts
|
||||
exc_rate_primary_to_default = (
|
||||
1
|
||||
if primary_account_currency == default_currency
|
||||
else get_exchange_rate(primary_account_currency, default_currency, self.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_default = (
|
||||
1
|
||||
if secondary_account_currency == default_currency
|
||||
else get_exchange_rate(secondary_account_currency, default_currency, self.posting_date)
|
||||
)
|
||||
exc_rate_secondary_to_primary = (
|
||||
1
|
||||
if secondary_account_currency == primary_account_currency
|
||||
else get_exchange_rate(
|
||||
secondary_account_currency, primary_account_currency, self.posting_date
|
||||
)
|
||||
)
|
||||
|
||||
# Convert outstanding amount from secondary to primary account currency, if needed
|
||||
|
||||
os_in_default_currency = self.outstanding_amount * exc_rate_secondary_to_default
|
||||
os_in_primary_currency = self.outstanding_amount * exc_rate_secondary_to_primary
|
||||
|
||||
if self.doctype == "Sales Invoice":
|
||||
# Calculate credit and debit values for reconciliation and advance entries
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.credit = os_in_default_currency
|
||||
|
||||
advance_entry.debit_in_account_currency = os_in_primary_currency
|
||||
advance_entry.debit = os_in_default_currency
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = os_in_primary_currency
|
||||
advance_entry.credit = os_in_default_currency
|
||||
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit = os_in_default_currency
|
||||
|
||||
# Set exchange rates for entries
|
||||
reconcilation_entry.exchange_rate = exc_rate_secondary_to_default
|
||||
advance_entry.exchange_rate = exc_rate_primary_to_default
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
default_currency = erpnext.get_company_currency(self.company)
|
||||
if primary_account_currency != default_currency or secondary_account_currency != default_currency:
|
||||
jv.multi_currency = 1
|
||||
if self.doctype == "Sales Invoice":
|
||||
reconcilation_entry.credit_in_account_currency = self.outstanding_amount
|
||||
advance_entry.debit_in_account_currency = self.outstanding_amount
|
||||
else:
|
||||
advance_entry.credit_in_account_currency = self.outstanding_amount
|
||||
reconcilation_entry.debit_in_account_currency = self.outstanding_amount
|
||||
|
||||
jv.multi_currency = multi_currency
|
||||
jv.append("accounts", reconcilation_entry)
|
||||
jv.append("accounts", advance_entry)
|
||||
|
||||
|
||||
@@ -356,14 +356,14 @@ class BuyingController(SubcontractingController):
|
||||
if not self.is_internal_transfer():
|
||||
return
|
||||
|
||||
self.set_sales_incoming_rate_for_internal_transfer()
|
||||
|
||||
allow_at_arms_length_price = frappe.get_cached_value(
|
||||
"Stock Settings", None, "allow_internal_transfer_at_arms_length_price"
|
||||
)
|
||||
if allow_at_arms_length_price:
|
||||
return
|
||||
|
||||
self.set_sales_incoming_rate_for_internal_transfer()
|
||||
|
||||
for d in self.get("items"):
|
||||
d.discount_percentage = 0.0
|
||||
d.discount_amount = 0.0
|
||||
|
||||
@@ -11,7 +11,13 @@ def set_print_templates_for_item_table(doc, settings):
|
||||
"items": {
|
||||
"qty": "templates/print_formats/includes/item_table_qty.html",
|
||||
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
|
||||
}
|
||||
},
|
||||
"packed_items": {
|
||||
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
|
||||
},
|
||||
"supplied_items": {
|
||||
"serial_and_batch_bundle": "templates/print_formats/includes/serial_and_batch_bundle.html",
|
||||
},
|
||||
}
|
||||
|
||||
doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]
|
||||
|
||||
@@ -74,19 +74,13 @@ class SellingController(StockController):
|
||||
if customer:
|
||||
from erpnext.accounts.party import _get_party_details
|
||||
|
||||
fetch_payment_terms_template = False
|
||||
if self.get("__islocal") or self.company != frappe.db.get_value(
|
||||
self.doctype, self.name, "company"
|
||||
):
|
||||
fetch_payment_terms_template = True
|
||||
|
||||
party_details = _get_party_details(
|
||||
customer,
|
||||
ignore_permissions=self.flags.ignore_permissions,
|
||||
doctype=self.doctype,
|
||||
company=self.company,
|
||||
posting_date=self.get("posting_date"),
|
||||
fetch_payment_terms_template=fetch_payment_terms_template,
|
||||
fetch_payment_terms_template=self.has_value_changed("company"),
|
||||
party_address=self.customer_address,
|
||||
shipping_address=self.shipping_address_name,
|
||||
company_address=self.get("company_address"),
|
||||
@@ -381,12 +375,32 @@ class SellingController(StockController):
|
||||
return il
|
||||
|
||||
def has_product_bundle(self, item_code):
|
||||
product_bundle = frappe.qb.DocType("Product Bundle")
|
||||
return (
|
||||
frappe.qb.from_(product_bundle)
|
||||
.select(product_bundle.name)
|
||||
.where((product_bundle.new_item_code == item_code) & (product_bundle.disabled == 0))
|
||||
).run()
|
||||
product_bundle_items = getattr(self, "_product_bundle_items", None)
|
||||
if product_bundle_items is None:
|
||||
self._product_bundle_items = product_bundle_items = {}
|
||||
|
||||
if item_code not in product_bundle_items:
|
||||
self._fetch_product_bundle_items(item_code)
|
||||
|
||||
return product_bundle_items[item_code]
|
||||
|
||||
def _fetch_product_bundle_items(self, item_code):
|
||||
product_bundle_items = self._product_bundle_items
|
||||
items_to_fetch = {row.item_code for row in self.items if row.item_code not in product_bundle_items}
|
||||
# fetch for requisite item_code even if it is not in items
|
||||
items_to_fetch.add(item_code)
|
||||
|
||||
items_with_product_bundle = {
|
||||
row.new_item_code
|
||||
for row in frappe.get_all(
|
||||
"Product Bundle",
|
||||
filters={"new_item_code": ("in", items_to_fetch), "disabled": 0},
|
||||
fields="new_item_code",
|
||||
)
|
||||
}
|
||||
|
||||
for item_code in items_to_fetch:
|
||||
product_bundle_items[item_code] = item_code in items_with_product_bundle
|
||||
|
||||
def get_already_delivered_qty(self, current_docname, so, so_detail):
|
||||
delivered_via_dn = frappe.db.sql(
|
||||
|
||||
@@ -807,6 +807,7 @@ class TestAccountsController(FrappeTestCase):
|
||||
|
||||
@change_settings("Stock Settings", {"allow_internal_transfer_at_arms_length_price": 1})
|
||||
def test_16_internal_transfer_at_arms_length_price(self):
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_purchase_invoice
|
||||
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
|
||||
|
||||
prepare_data_for_internal_transfer()
|
||||
@@ -840,6 +841,31 @@ class TestAccountsController(FrappeTestCase):
|
||||
# rate should reset to incoming rate
|
||||
self.assertEqual(si.items[0].rate, 100)
|
||||
|
||||
si.update_stock = 0
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].rate = arms_length_price
|
||||
pi.items[0].warehouse = target_warehouse
|
||||
pi.items[0].from_warehouse = warehouse
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.items[0].rate, 100)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
frappe.db.set_single_value("Stock Settings", "allow_internal_transfer_at_arms_length_price", 1)
|
||||
pi = make_inter_company_purchase_invoice(si.name)
|
||||
pi.update_stock = 1
|
||||
pi.items[0].rate = arms_length_price
|
||||
pi.items[0].warehouse = target_warehouse
|
||||
pi.items[0].from_warehouse = warehouse
|
||||
pi.save()
|
||||
|
||||
self.assertEqual(pi.items[0].rate, arms_length_price)
|
||||
self.assertEqual(pi.items[0].valuation_rate, 100)
|
||||
|
||||
def test_20_journal_against_sales_invoice(self):
|
||||
# Invoice in Foreign Currency
|
||||
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)
|
||||
|
||||
@@ -160,10 +160,18 @@ class WorkOrder(Document):
|
||||
self.validate_workstation_type()
|
||||
self.reset_use_multi_level_bom()
|
||||
|
||||
if self.source_warehouse:
|
||||
self.set_warehouses()
|
||||
|
||||
validate_uom_is_integer(self, "stock_uom", ["qty", "produced_qty"])
|
||||
|
||||
self.set_required_items(reset_only_qty=len(self.get("required_items")))
|
||||
|
||||
def set_warehouses(self):
|
||||
for row in self.required_items:
|
||||
if not row.source_warehouse:
|
||||
row.source_warehouse = self.source_warehouse
|
||||
|
||||
def reset_use_multi_level_bom(self):
|
||||
if self.is_new():
|
||||
return
|
||||
|
||||
@@ -383,3 +383,4 @@ erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
||||
erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
|
||||
erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
|
||||
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import frappe
|
||||
|
||||
|
||||
def execute():
|
||||
default_exchange_gain_loss_accounts = frappe.get_all(
|
||||
"Company",
|
||||
filters={"exchange_gain_loss_account": ["!=", ""]},
|
||||
pluck="exchange_gain_loss_account",
|
||||
)
|
||||
|
||||
if not default_exchange_gain_loss_accounts:
|
||||
return
|
||||
|
||||
payment_entry = frappe.qb.DocType("Payment Entry")
|
||||
payment_entry_deduction = frappe.qb.DocType("Payment Entry Deduction")
|
||||
|
||||
frappe.qb.update(payment_entry_deduction).set(payment_entry_deduction.is_exchange_gain_loss, 1).join(
|
||||
payment_entry,
|
||||
).on(payment_entry.name == payment_entry_deduction.parent).where(
|
||||
(payment_entry.paid_to_account_currency != payment_entry.paid_from_account_currency)
|
||||
& (payment_entry_deduction.account.isin(default_exchange_gain_loss_accounts))
|
||||
).run()
|
||||
@@ -486,7 +486,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
setup_sms() {
|
||||
var me = this;
|
||||
let blacklist = ['Purchase Invoice', 'BOM'];
|
||||
if(this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status)
|
||||
if(frappe.boot.sms_gateway_enabled && this.frm.doc.docstatus===1 && !["Lost", "Stopped", "Closed"].includes(this.frm.doc.status)
|
||||
&& !blacklist.includes(this.frm.doctype)) {
|
||||
this.frm.page.add_menu_item(__('Send SMS'), function() { me.send_sms(); });
|
||||
}
|
||||
@@ -1124,7 +1124,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe
|
||||
|
||||
apply_discount_on_item(doc, cdt, cdn, field) {
|
||||
var item = frappe.get_doc(cdt, cdn);
|
||||
if(!item?.price_list_rate) {
|
||||
if(item && !item.price_list_rate) {
|
||||
item[field] = 0.0;
|
||||
} else {
|
||||
this.price_list_rate(doc, cdt, cdn);
|
||||
|
||||
@@ -9,25 +9,20 @@ erpnext.financial_statements = {
|
||||
data &&
|
||||
column.colIndex >= 3
|
||||
) {
|
||||
//Assuming that the first three columns are s.no, account name and the very first year of the accounting values, to calculate the relative percentage values of the successive columns.
|
||||
const lastAnnualValue = row[column.colIndex - 1].content;
|
||||
const currentAnnualvalue = data[column.fieldname];
|
||||
if (currentAnnualvalue == undefined) return "NA"; //making this not applicable for undefined/null values
|
||||
let annualGrowth = 0;
|
||||
if (lastAnnualValue == 0 && currentAnnualvalue > 0) {
|
||||
//If the previous year value is 0 and the current value is greater than 0
|
||||
annualGrowth = 1;
|
||||
} else if (lastAnnualValue > 0) {
|
||||
annualGrowth = (currentAnnualvalue - lastAnnualValue) / lastAnnualValue;
|
||||
}
|
||||
const growthPercent = data[column.fieldname];
|
||||
|
||||
const growthPercent = Math.round(annualGrowth * 10000) / 100; //calculating the rounded off percentage
|
||||
if (growthPercent == undefined) return "NA"; //making this not applicable for undefined/null values
|
||||
|
||||
value = $(`<span>${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}</span>`);
|
||||
if (growthPercent < 0) {
|
||||
value = $(value).addClass("text-danger");
|
||||
if (column.fieldname === "total") {
|
||||
value = $(`<span>${growthPercent}</span>`);
|
||||
} else {
|
||||
value = $(value).addClass("text-success");
|
||||
value = $(`<span>${(growthPercent >= 0 ? "+" : "") + growthPercent + "%"}</span>`);
|
||||
|
||||
if (growthPercent < 0) {
|
||||
value = $(value).addClass("text-danger");
|
||||
} else {
|
||||
value = $(value).addClass("text-success");
|
||||
}
|
||||
}
|
||||
value = $(value).wrap("<p></p>").parent().html();
|
||||
|
||||
@@ -38,11 +33,9 @@ erpnext.financial_statements = {
|
||||
this.baseData = row;
|
||||
}
|
||||
if (column.colIndex >= 2) {
|
||||
//Assuming that the first two columns are s.no and account name, to calculate the relative percentage values of the successive columns.
|
||||
const currentAnnualvalue = data[column.fieldname];
|
||||
const baseValue = this.baseData[column.colIndex].content;
|
||||
if (currentAnnualvalue == undefined || baseValue <= 0) return "NA";
|
||||
const marginPercent = Math.round((currentAnnualvalue / baseValue) * 10000) / 100;
|
||||
const marginPercent = data[column.fieldname];
|
||||
|
||||
if (marginPercent == undefined) return "NA"; //making this not applicable for undefined/null values
|
||||
|
||||
value = $(`<span>${marginPercent + "%"}</span>`);
|
||||
if (marginPercent < 0) value = $(value).addClass("text-danger");
|
||||
@@ -53,7 +46,8 @@ erpnext.financial_statements = {
|
||||
}
|
||||
|
||||
if (data && column.fieldname == "account") {
|
||||
value = data.account_name || value;
|
||||
// first column
|
||||
value = data.section_name || data.account_name || value;
|
||||
|
||||
if (filter && filter?.text && filter?.type == "contains") {
|
||||
if (!value.toLowerCase().includes(filter.text)) {
|
||||
@@ -61,7 +55,7 @@ erpnext.financial_statements = {
|
||||
}
|
||||
}
|
||||
|
||||
if (data.account) {
|
||||
if (data.account || data.accounts) {
|
||||
column.link_onclick =
|
||||
"erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")";
|
||||
}
|
||||
@@ -70,7 +64,7 @@ erpnext.financial_statements = {
|
||||
|
||||
value = default_formatter(value, row, column, data);
|
||||
|
||||
if (data && !data.parent_account) {
|
||||
if (data && !data.parent_account && !data.parent_section) {
|
||||
value = $(`<span>${value}</span>`);
|
||||
|
||||
var $value = $(value).css("font-weight", "bold");
|
||||
@@ -84,13 +78,13 @@ erpnext.financial_statements = {
|
||||
return value;
|
||||
},
|
||||
open_general_ledger: function (data) {
|
||||
if (!data.account) return;
|
||||
if (!data.account && !data.accounts) return;
|
||||
let project = $.grep(frappe.query_report.filters, function (e) {
|
||||
return e.df.fieldname == "project";
|
||||
});
|
||||
|
||||
frappe.route_options = {
|
||||
account: data.account,
|
||||
account: data.account || data.accounts,
|
||||
company: frappe.query_report.get_filter_value("company"),
|
||||
from_date: data.from_date || data.year_start_date,
|
||||
to_date: data.to_date || data.year_end_date,
|
||||
|
||||
@@ -56,6 +56,17 @@ $.extend(erpnext.queries, {
|
||||
}
|
||||
},
|
||||
|
||||
company_contact_query: function (doc) {
|
||||
if (!doc.company) {
|
||||
frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, "company", doc.name))]));
|
||||
}
|
||||
|
||||
return {
|
||||
query: "frappe.contacts.doctype.contact.contact.contact_query",
|
||||
filters: { link_doctype: "Company", link_name: doc.company },
|
||||
};
|
||||
},
|
||||
|
||||
address_query: function (doc) {
|
||||
if (frappe.dynamic_link) {
|
||||
if (!doc[frappe.dynamic_link.fieldname]) {
|
||||
|
||||
@@ -49,6 +49,7 @@ erpnext.sales_common = {
|
||||
);
|
||||
|
||||
me.frm.set_query("contact_person", erpnext.queries.contact_query);
|
||||
me.frm.set_query("company_contact_person", erpnext.queries.company_contact_query);
|
||||
me.frm.set_query("customer_address", erpnext.queries.address_query);
|
||||
me.frm.set_query("shipping_address_name", erpnext.queries.address_query);
|
||||
me.frm.set_query("dispatch_address_name", erpnext.queries.dispatch_address_query);
|
||||
|
||||
@@ -96,8 +96,9 @@
|
||||
"shipping_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"column_break_87",
|
||||
"company_address_display",
|
||||
"column_break_87",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_template",
|
||||
@@ -1076,13 +1077,20 @@
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-shopping-cart",
|
||||
"idx": 82,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-11-07 18:37:11.715189",
|
||||
"modified": "2024-11-26 12:43:29.293637",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Quotation",
|
||||
|
||||
@@ -50,6 +50,7 @@ class Quotation(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
competitors: DF.TableMultiSelect[CompetitorDetail]
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
@@ -568,7 +569,7 @@ def handle_mandatory_error(e, customer, lead_name):
|
||||
from frappe.utils import get_link_to_form
|
||||
|
||||
mandatory_fields = e.args[0].split(":")[1].split(",")
|
||||
mandatory_fields = [customer.meta.get_label(field.strip()) for field in mandatory_fields]
|
||||
mandatory_fields = [_(customer.meta.get_label(field.strip())) for field in mandatory_fields]
|
||||
|
||||
frappe.local.message_log = []
|
||||
message = _("Could not auto create Customer due to the following missing mandatory field(s):") + "<br>"
|
||||
|
||||
@@ -113,8 +113,9 @@
|
||||
"dispatch_address",
|
||||
"col_break46",
|
||||
"company_address",
|
||||
"column_break_92",
|
||||
"company_address_display",
|
||||
"column_break_92",
|
||||
"company_contact_person",
|
||||
"payment_schedule_section",
|
||||
"payment_terms_section",
|
||||
"payment_terms_template",
|
||||
@@ -1640,13 +1641,20 @@
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-23 16:35:54.905804",
|
||||
"modified": "2024-11-26 12:42:06.872527",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
||||
@@ -85,6 +85,7 @@ class SalesOrder(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
contact_mobile: DF.SmallText | None
|
||||
|
||||
@@ -48,24 +48,30 @@
|
||||
"default_bank_account",
|
||||
"default_cash_account",
|
||||
"default_receivable_account",
|
||||
"round_off_account",
|
||||
"round_off_for_opening",
|
||||
"round_off_cost_center",
|
||||
"default_payable_account",
|
||||
"write_off_account",
|
||||
"exchange_gain_loss_account",
|
||||
"unrealized_exchange_gain_loss_account",
|
||||
"unrealized_profit_loss_account",
|
||||
"column_break0",
|
||||
"allow_account_creation_against_child_company",
|
||||
"default_payable_account",
|
||||
"default_expense_account",
|
||||
"default_income_account",
|
||||
"default_deferred_revenue_account",
|
||||
"default_deferred_expense_account",
|
||||
"default_discount_account",
|
||||
"payment_terms",
|
||||
"cost_center",
|
||||
"default_finance_book",
|
||||
"exchange_gain__loss_section",
|
||||
"exchange_gain_loss_account",
|
||||
"column_break_sttp",
|
||||
"unrealized_exchange_gain_loss_account",
|
||||
"round_off_section",
|
||||
"round_off_account",
|
||||
"round_off_cost_center",
|
||||
"column_break_jqfo",
|
||||
"round_off_for_opening",
|
||||
"deferred_accounting_section",
|
||||
"default_deferred_revenue_account",
|
||||
"column_break_dcdl",
|
||||
"default_deferred_expense_account",
|
||||
"advance_payments_section",
|
||||
"book_advance_payments_in_separate_party_account",
|
||||
"reconcile_on_advance_payment_date",
|
||||
@@ -287,7 +293,7 @@
|
||||
{
|
||||
"fieldname": "default_settings",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounts Settings",
|
||||
"label": "Default Accounts",
|
||||
"oldfieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
@@ -808,6 +814,33 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Round Off for Opening",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_gain__loss_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Exchange Gain / Loss"
|
||||
},
|
||||
{
|
||||
"fieldname": "round_off_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Round Off"
|
||||
},
|
||||
{
|
||||
"fieldname": "deferred_accounting_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Deferred Accounting"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sttp",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_jqfo",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_dcdl",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-building",
|
||||
@@ -815,7 +848,7 @@
|
||||
"image_field": "company_logo",
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"modified": "2024-08-02 11:34:46.785377",
|
||||
"modified": "2024-12-02 15:37:32.723176",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Setup",
|
||||
"name": "Company",
|
||||
|
||||
@@ -108,8 +108,9 @@
|
||||
"dispatch_address",
|
||||
"company_address_section",
|
||||
"company_address",
|
||||
"column_break_101",
|
||||
"company_address_display",
|
||||
"column_break_101",
|
||||
"company_contact_person",
|
||||
"terms_tab",
|
||||
"tc_name",
|
||||
"terms",
|
||||
@@ -1391,13 +1392,20 @@
|
||||
"fieldname": "named_place",
|
||||
"fieldtype": "Data",
|
||||
"label": "Named Place"
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-20 16:05:02.854990",
|
||||
"modified": "2024-11-26 12:44:28.258215",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
||||
@@ -53,6 +53,7 @@ class DeliveryNote(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.SmallText | None
|
||||
company_contact_person: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
contact_mobile: DF.SmallText | None
|
||||
|
||||
@@ -1085,7 +1085,7 @@ def update_billing_percentage(pr_doc, update_modified=True, adjust_incoming_rate
|
||||
|
||||
if adjust_incoming_rate:
|
||||
adjusted_amt = 0.0
|
||||
if item.billed_amt and item.amount:
|
||||
if item.billed_amt is not None and item.amount is not None:
|
||||
adjusted_amt = flt(item.billed_amt) - flt(item.amount)
|
||||
|
||||
adjusted_amt = adjusted_amt * flt(pr_doc.conversion_rate)
|
||||
|
||||
@@ -432,8 +432,6 @@ class SerialandBatchBundle(Document):
|
||||
valuation_field = "rate"
|
||||
child_table = "Subcontracting Receipt Item"
|
||||
|
||||
precision = frappe.get_precision(child_table, valuation_field) or 2
|
||||
|
||||
if not rate and self.voucher_detail_no and self.voucher_no:
|
||||
rate = frappe.db.get_value(child_table, self.voucher_detail_no, valuation_field)
|
||||
|
||||
@@ -443,9 +441,9 @@ class SerialandBatchBundle(Document):
|
||||
elif (d.incoming_rate == rate) and d.qty and d.stock_value_difference:
|
||||
continue
|
||||
|
||||
d.incoming_rate = flt(rate, precision)
|
||||
d.incoming_rate = rate
|
||||
if d.qty:
|
||||
d.stock_value_difference = flt(d.qty) * flt(d.incoming_rate)
|
||||
d.stock_value_difference = d.qty * d.incoming_rate
|
||||
|
||||
if save:
|
||||
d.db_set(
|
||||
@@ -609,8 +607,10 @@ class SerialandBatchBundle(Document):
|
||||
|
||||
precision = row.precision
|
||||
if abs(abs(flt(self.total_qty, precision)) - abs(flt(qty, precision))) > 0.01:
|
||||
total_qty = frappe.format_value(abs(flt(self.total_qty)), "Float", row)
|
||||
set_qty = frappe.format_value(abs(flt(row.get(qty_field))), "Float", row)
|
||||
self.throw_error_message(
|
||||
f"Total quantity {abs(flt(self.total_qty))} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {abs(flt(row.get(qty_field)))} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}"
|
||||
f"Total quantity {total_qty} in the Serial and Batch Bundle {bold(self.name)} does not match with the quantity {set_qty} for the Item {bold(self.item_code)} in the {self.voucher_type} # {self.voucher_no}"
|
||||
)
|
||||
|
||||
def get_qty_field(self, row, qty_field=None) -> str:
|
||||
@@ -972,6 +972,9 @@ class SerialandBatchBundle(Document):
|
||||
):
|
||||
return
|
||||
|
||||
if self.voucher_type in ["Sales Invoice", "Delivery Note"] and self.type_of_transaction == "Inward":
|
||||
return
|
||||
|
||||
if not self.has_batch_no:
|
||||
return
|
||||
|
||||
|
||||
@@ -478,7 +478,7 @@ def get_serial_or_batch_nos(bundle):
|
||||
html = "<table class= 'table table-borderless' style='margin-top: 0px;margin-bottom: 0px;'>"
|
||||
for d in data:
|
||||
if d.serial_no:
|
||||
html += f"<tr><td>{d.batch_no}</th><th>{d.serial_no}</th ><th>{abs(d.qty)}</th></tr>"
|
||||
html += f"<tr><td>{d.batch_no}</td><td>{d.serial_no}</td><td>{abs(d.qty)}</td></tr>"
|
||||
else:
|
||||
html += f"<tr><td>{d.batch_no}</td><td>{abs(d.qty)}</td></tr>"
|
||||
|
||||
|
||||
@@ -257,11 +257,11 @@ def validate_uom_is_integer(doc, uom_field, qty_fields, child_dt=None):
|
||||
if isinstance(qty_fields, str):
|
||||
qty_fields = [qty_fields]
|
||||
|
||||
distinct_uoms = list(set(d.get(uom_field) for d in doc.get_all_children()))
|
||||
integer_uoms = list(
|
||||
filter(
|
||||
lambda uom: frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True) or None,
|
||||
distinct_uoms,
|
||||
distinct_uoms = tuple(set(uom for uom in (d.get(uom_field) for d in doc.get_all_children()) if uom))
|
||||
integer_uoms = set(
|
||||
d[0]
|
||||
for d in frappe.db.get_values(
|
||||
"UOM", (("name", "in", distinct_uoms), ("must_be_whole_number", "=", 1)), cache=True
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user