mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-03 21:50:53 +00:00
Compare commits
121 Commits
refactor-p
...
ledger_rep
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ed231abb54 | ||
|
|
024c442087 | ||
|
|
7249cf0001 | ||
|
|
322c788760 | ||
|
|
61439132a4 | ||
|
|
14f2b0ab0e | ||
|
|
5413cf9f1f | ||
|
|
707d8eddc5 | ||
|
|
abd2314894 | ||
|
|
fb5cbc43a2 | ||
|
|
31efaf6dbf | ||
|
|
2f38390b48 | ||
|
|
138ffc4e93 | ||
|
|
ba8ba79335 | ||
|
|
353610ce61 | ||
|
|
bd77a5557d | ||
|
|
163ff71ece | ||
|
|
6a9a4f10f0 | ||
|
|
663b66ca1d | ||
|
|
029dc948fe | ||
|
|
4001166ecc | ||
|
|
b4534e56e4 | ||
|
|
4b72b60f1a | ||
|
|
23c846d4b9 | ||
|
|
d544328ffe | ||
|
|
6585fabdb1 | ||
|
|
e6be1021f9 | ||
|
|
f74c99be9d | ||
|
|
032ef62b14 | ||
|
|
657c85638c | ||
|
|
483fd124fc | ||
|
|
c3ace82db8 | ||
|
|
88b0af1696 | ||
|
|
fc0122ce76 | ||
|
|
d0c522ee46 | ||
|
|
f11eab06c3 | ||
|
|
9bbb953e26 | ||
|
|
f21283d829 | ||
|
|
94de5c4e7e | ||
|
|
dc645b3906 | ||
|
|
f758dfcbe1 | ||
|
|
fdfd51c0fd | ||
|
|
7244754d28 | ||
|
|
4e6a5893e7 | ||
|
|
7cc111f790 | ||
|
|
7de9c14a2c | ||
|
|
a86b223aed | ||
|
|
810a2c8fc1 | ||
|
|
6b6798bee6 | ||
|
|
e355b18c0c | ||
|
|
a4f8315602 | ||
|
|
6df0ea153d | ||
|
|
74624828e7 | ||
|
|
55cb1c54e0 | ||
|
|
bd042d0fff | ||
|
|
aa8254963c | ||
|
|
04c7c0bb66 | ||
|
|
279dcabf38 | ||
|
|
9ee4f58e1b | ||
|
|
b6b8a06fda | ||
|
|
6de7320ef4 | ||
|
|
b80022133c | ||
|
|
810c72a30c | ||
|
|
9bee2d430c | ||
|
|
de6cbd382f | ||
|
|
561a159aec | ||
|
|
7f7564b581 | ||
|
|
4050ea07eb | ||
|
|
5266f236b7 | ||
|
|
d37d7b9811 | ||
|
|
76bd1017f4 | ||
|
|
2b3c829662 | ||
|
|
da5dba997d | ||
|
|
2394da419e | ||
|
|
3182c6981c | ||
|
|
e371f68d66 | ||
|
|
44c33cd12a | ||
|
|
4066df8652 | ||
|
|
a6d8383e43 | ||
|
|
69bd90b038 | ||
|
|
5de7db2be0 | ||
|
|
46ce8780f2 | ||
|
|
af5dae8682 | ||
|
|
1737de7c10 | ||
|
|
7c393e5aa0 | ||
|
|
9a5d68f1f4 | ||
|
|
f6776c7d6b | ||
|
|
ea6ddd5df6 | ||
|
|
e7808981cf | ||
|
|
78a1b211a1 | ||
|
|
4d164d5854 | ||
|
|
21049bae91 | ||
|
|
82907672d9 | ||
|
|
7efe05baf2 | ||
|
|
46b15f6040 | ||
|
|
3de5ce74e1 | ||
|
|
676c93411e | ||
|
|
214dfab269 | ||
|
|
e1b3193b04 | ||
|
|
f42ec6a124 | ||
|
|
e3770bc9e1 | ||
|
|
4dbd8054e8 | ||
|
|
1ea5c5d821 | ||
|
|
c9fb59a158 | ||
|
|
02225e6a33 | ||
|
|
ebfbee3da5 | ||
|
|
eff9cd10cd | ||
|
|
94d3fc9fde | ||
|
|
d049c97884 | ||
|
|
e2bae4cf07 | ||
|
|
8b5d4c0236 | ||
|
|
bbe3bc95d0 | ||
|
|
38e7d0a41e | ||
|
|
6e19c06e58 | ||
|
|
af5a3e5a48 | ||
|
|
00403515a8 | ||
|
|
8a42601e99 | ||
|
|
af9524920b | ||
|
|
4bdc6a0021 | ||
|
|
87065d0387 | ||
|
|
e3d734c890 |
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,34 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
syscohada_countries = [
|
||||
"bj", # Bénin
|
||||
"bf", # Burkina-Faso
|
||||
"cm", # Cameroun
|
||||
"cf", # Centrafrique
|
||||
"ci", # Côte d'Ivoire
|
||||
"cg", # Congo
|
||||
"km", # Comores
|
||||
"ga", # Gabon
|
||||
"gn", # Guinée
|
||||
"gw", # Guinée-Bissau
|
||||
"gq", # Guinée Equatoriale
|
||||
"ml", # Mali
|
||||
"ne", # Niger
|
||||
"cd", # République Démocratique du Congo
|
||||
"sn", # Sénégal
|
||||
"td", # Tchad
|
||||
"tg", # Togo
|
||||
]
|
||||
|
||||
folder = Path(__file__).parent
|
||||
generic_charts = Path(folder).glob("syscohada*.json")
|
||||
|
||||
for file in generic_charts:
|
||||
with open(file) as f:
|
||||
chart = json.load(f)
|
||||
for country in syscohada_countries:
|
||||
chart["country_code"] = country
|
||||
json_object = json.dumps(chart, indent=4)
|
||||
with open(Path(folder, file.name.replace("syscohada", country)), "w") as outfile:
|
||||
outfile.write(json_object)
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -188,7 +188,7 @@ class TestExchangeRateRevaluation(AccountsTestMixin, IntegrationTestCase):
|
||||
|
||||
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, IntegrationTestCase):
|
||||
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()
|
||||
|
||||
@@ -324,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) {
|
||||
@@ -1119,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;
|
||||
}
|
||||
}
|
||||
@@ -1242,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) {
|
||||
@@ -1778,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);
|
||||
},
|
||||
@@ -1799,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",
|
||||
@@ -391,11 +390,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"
|
||||
@@ -802,7 +796,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",
|
||||
|
||||
@@ -985,6 +985,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):
|
||||
@@ -1083,10 +1084,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):
|
||||
@@ -1104,29 +1105,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) * (
|
||||
@@ -1154,11 +1206,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
|
||||
|
||||
@@ -1318,11 +1372,19 @@ class PaymentEntry(AccountsController):
|
||||
dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
|
||||
|
||||
gle.update(
|
||||
{
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
"cost_center": cost_center,
|
||||
}
|
||||
self.get_gl_dict(
|
||||
{
|
||||
"account": self.party_account,
|
||||
"party_type": self.party_type,
|
||||
"party": self.party,
|
||||
"against": against_account,
|
||||
"account_currency": self.party_account_currency,
|
||||
"cost_center": cost_center,
|
||||
dr_or_cr + "_in_account_currency": d.allocated_amount,
|
||||
dr_or_cr: allocated_amount_in_company_currency,
|
||||
},
|
||||
item=self,
|
||||
)
|
||||
)
|
||||
|
||||
if self.book_advance_payments_in_separate_party_account:
|
||||
@@ -2006,8 +2068,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), ...]
|
||||
"""
|
||||
@@ -2109,7 +2171,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, ...}
|
||||
@@ -2923,9 +2985,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"):
|
||||
@@ -2935,7 +2994,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:
|
||||
@@ -2947,7 +3007,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}}
|
||||
"""
|
||||
@@ -2971,6 +3031,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)
|
||||
|
||||
@@ -3285,13 +3346,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,
|
||||
}
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -488,16 +488,9 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
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()
|
||||
@@ -561,16 +554,10 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
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()
|
||||
|
||||
@@ -660,16 +647,9 @@ class TestPaymentEntry(IntegrationTestCase):
|
||||
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": "2024-03-27 13:10:09.454552",
|
||||
"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
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder.functions import Sum
|
||||
from frappe.query_builder.functions import Abs, Sum
|
||||
from frappe.utils import flt, nowdate
|
||||
from frappe.utils.background_jobs import enqueue
|
||||
|
||||
@@ -563,6 +563,8 @@ def make_payment_request(**args):
|
||||
# fetches existing payment request `grand_total` amount
|
||||
existing_payment_request_amount = get_existing_payment_request_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
existing_paid_amount = get_existing_paid_amount(ref_doc.doctype, ref_doc.name)
|
||||
|
||||
def validate_and_calculate_grand_total(grand_total, existing_payment_request_amount):
|
||||
grand_total -= existing_payment_request_amount
|
||||
if not grand_total:
|
||||
@@ -582,6 +584,15 @@ def make_payment_request(**args):
|
||||
else:
|
||||
grand_total = validate_and_calculate_grand_total(grand_total, existing_payment_request_amount)
|
||||
|
||||
if existing_paid_amount:
|
||||
if ref_doc.party_account_currency == ref_doc.currency:
|
||||
if ref_doc.conversion_rate:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount)
|
||||
else:
|
||||
grand_total -= flt(existing_paid_amount / ref_doc.conversion_rate)
|
||||
|
||||
if draft_payment_request:
|
||||
frappe.db.set_value(
|
||||
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
|
||||
@@ -758,6 +769,29 @@ def get_existing_payment_request_amount(ref_dt, ref_dn, statuses: list | None =
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_existing_paid_amount(doctype, name):
|
||||
PL = frappe.qb.DocType("Payment Ledger Entry")
|
||||
PER = frappe.qb.DocType("Payment Entry Reference")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PL)
|
||||
.left_join(PER)
|
||||
.on(
|
||||
(PER.reference_doctype == PL.against_voucher_type) & (PER.reference_name == PL.against_voucher_no)
|
||||
)
|
||||
.select(Abs(Sum(PL.amount)).as_("total_paid_amount"))
|
||||
.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()
|
||||
|
||||
return response[0][0] if response[0] else 0
|
||||
|
||||
|
||||
def get_gateway_details(args): # nosemgrep
|
||||
"""
|
||||
Return gateway and payment account of default payment gateway
|
||||
|
||||
@@ -8,6 +8,7 @@ from unittest.mock import patch
|
||||
import frappe
|
||||
from frappe.tests import IntegrationTestCase, UnitTestCase
|
||||
|
||||
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_terms_template
|
||||
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
|
||||
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
|
||||
@@ -689,3 +690,48 @@ class TestPaymentRequest(IntegrationTestCase):
|
||||
|
||||
so.load_from_db()
|
||||
self.assertEqual(so.advance_payment_status, "Requested")
|
||||
|
||||
def test_partial_paid_invoice_with_payment_request(self):
|
||||
si = create_sales_invoice(currency="INR", qty=1, rate=5000)
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "PAYEE0002"
|
||||
pe.reference_date = frappe.utils.nowdate()
|
||||
pe.paid_amount = 2500
|
||||
pe.references[0].allocated_amount = 2500
|
||||
pe.save()
|
||||
pe.submit()
|
||||
|
||||
si.load_from_db()
|
||||
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)
|
||||
|
||||
@@ -171,9 +171,7 @@ class PeriodClosingVoucher(AccountsController):
|
||||
pl_account_balances = self.get_account_balances_based_on_dimensions(report_type="Profit and Loss")
|
||||
for dimensions, account_balances in pl_account_balances.items():
|
||||
for acc, balances in account_balances.items():
|
||||
balance_in_company_currency = flt(balances.debit_in_account_currency) - flt(
|
||||
balances.credit_in_account_currency
|
||||
)
|
||||
balance_in_company_currency = flt(balances.debit) - flt(balances.credit)
|
||||
if balance_in_company_currency and acc != "balances":
|
||||
self.pl_accounts_reverse_gle.append(
|
||||
self.get_gle_for_pl_account(acc, balances, dimensions)
|
||||
|
||||
@@ -147,7 +147,7 @@ frappe.ui.form.on("POS Closing Entry", {
|
||||
frm.doc.grand_total += flt(doc.grand_total);
|
||||
frm.doc.net_total += flt(doc.net_total);
|
||||
frm.doc.total_quantity += flt(doc.total_qty);
|
||||
refresh_payments(doc, frm);
|
||||
refresh_payments(doc, frm, false);
|
||||
refresh_taxes(doc, frm);
|
||||
refresh_fields(frm);
|
||||
set_html_data(frm);
|
||||
@@ -172,7 +172,7 @@ function set_form_data(data, frm) {
|
||||
frm.doc.grand_total += flt(d.grand_total);
|
||||
frm.doc.net_total += flt(d.net_total);
|
||||
frm.doc.total_quantity += flt(d.total_qty);
|
||||
refresh_payments(d, frm);
|
||||
refresh_payments(d, frm, true);
|
||||
refresh_taxes(d, frm);
|
||||
});
|
||||
}
|
||||
@@ -186,7 +186,7 @@ function add_to_pos_transaction(d, frm) {
|
||||
});
|
||||
}
|
||||
|
||||
function refresh_payments(d, frm) {
|
||||
function refresh_payments(d, frm, is_new) {
|
||||
d.payments.forEach((p) => {
|
||||
const payment = frm.doc.payment_reconciliation.find(
|
||||
(pay) => pay.mode_of_payment === p.mode_of_payment
|
||||
@@ -196,9 +196,7 @@ function refresh_payments(d, frm) {
|
||||
}
|
||||
if (payment) {
|
||||
payment.expected_amount += flt(p.amount);
|
||||
if (payment.closing_amount === 0) {
|
||||
payment.closing_amount = payment.expected_amount;
|
||||
}
|
||||
if (is_new) payment.closing_amount = payment.expected_amount;
|
||||
payment.difference = payment.closing_amount - payment.expected_amount;
|
||||
} else {
|
||||
frm.add_child("payment_reconciliation", {
|
||||
|
||||
@@ -48,6 +48,7 @@
|
||||
"shipping_address",
|
||||
"company_address",
|
||||
"company_address_display",
|
||||
"company_contact_person",
|
||||
"currency_and_price_list",
|
||||
"currency",
|
||||
"conversion_rate",
|
||||
@@ -1549,10 +1550,10 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_medium",
|
||||
"print_hide": 1,
|
||||
"fieldtype": "Link",
|
||||
"label": "Medium",
|
||||
"options": "UTM Medium"
|
||||
"options": "UTM Medium",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "utm_campaign",
|
||||
@@ -1571,12 +1572,19 @@
|
||||
"oldfieldtype": "Select",
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"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-06-28 10:55:34.941200",
|
||||
"modified": "2024-11-26 13:10:50.309570",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice",
|
||||
@@ -1630,4 +1638,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
@@ -74,6 +70,7 @@ class POSInvoice(SalesInvoice):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.TextEditor | None
|
||||
company_contact_person: DF.Link | None
|
||||
consolidated_invoice: DF.Link | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
|
||||
@@ -1742,6 +1742,30 @@ class TestPurchaseInvoice(IntegrationTestCase, 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)
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
import inspect
|
||||
|
||||
import frappe
|
||||
from frappe import _, qb
|
||||
from frappe.model.document import Document
|
||||
@@ -142,6 +144,8 @@ class RepostAccountingLedger(Document):
|
||||
|
||||
@frappe.whitelist()
|
||||
def start_repost(account_repost_doc=str) -> None:
|
||||
from erpnext.accounts.general_ledger import make_reverse_gl_entries
|
||||
|
||||
frappe.flags.through_repost_accounting_ledger = True
|
||||
if account_repost_doc:
|
||||
repost_doc = frappe.get_doc("Repost Accounting Ledger", account_repost_doc)
|
||||
@@ -177,6 +181,14 @@ def start_repost(account_repost_doc=str) -> None:
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
doc.make_gl_entries(1)
|
||||
doc.make_gl_entries()
|
||||
else:
|
||||
if hasattr(doc, "make_gl_entries") and callable(doc.make_gl_entries):
|
||||
if not repost_doc.delete_cancelled_entries:
|
||||
if "cancel" in inspect.getfullargspec(doc.make_gl_entries):
|
||||
doc.make_gl_entries(cancel=1)
|
||||
else:
|
||||
make_reverse_gl_entries(voucher_type=doc.doctype, voucher_no=doc.name)
|
||||
doc.make_gl_entries()
|
||||
|
||||
|
||||
def get_allowed_types_from_settings():
|
||||
|
||||
@@ -161,8 +161,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",
|
||||
@@ -2203,6 +2204,13 @@
|
||||
"oldfieldtype": "Select",
|
||||
"options": "UTM Source",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "company_contact_person",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company Contact Person",
|
||||
"options": "Contact",
|
||||
"print_hide": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@@ -2215,7 +2223,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",
|
||||
@@ -2270,4 +2278,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -98,6 +98,7 @@ class SalesInvoice(SellingController):
|
||||
company: DF.Link
|
||||
company_address: DF.Link | None
|
||||
company_address_display: DF.TextEditor | None
|
||||
company_contact_person: DF.Link | None
|
||||
company_tax_id: DF.Data | None
|
||||
contact_display: DF.SmallText | None
|
||||
contact_email: DF.Data | None
|
||||
@@ -153,7 +154,6 @@ class SalesInvoice(SellingController):
|
||||
party_account_currency: DF.Link | None
|
||||
payment_schedule: DF.Table[PaymentSchedule]
|
||||
payment_terms_template: DF.Link | None
|
||||
payment_url: DF.Data | None
|
||||
payments: DF.Table[SalesInvoicePayment]
|
||||
plc_conversion_rate: DF.Float
|
||||
po_date: DF.Date | None
|
||||
@@ -1713,6 +1713,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()
|
||||
|
||||
@@ -4167,6 +4167,88 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
self.assertTrue(jv)
|
||||
self.assertEqual(jv[0], si.grand_total)
|
||||
|
||||
@IntegrationTestCase.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_invoice_remarks(self):
|
||||
si = frappe.copy_doc(self.globalTestRecords["Sales Invoice"][0])
|
||||
si.po_no = "Test PO"
|
||||
@@ -4194,6 +4276,20 @@ class TestSalesInvoice(IntegrationTestCase):
|
||||
|
||||
self.assertTrue(all([x == "Credit Note" for x in gl_entries]))
|
||||
|
||||
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, IntegrationTestCase):
|
||||
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, IntegrationTestCase):
|
||||
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",
|
||||
@@ -987,10 +993,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
|
||||
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
|
||||
import copy
|
||||
import functools
|
||||
import math
|
||||
import re
|
||||
@@ -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
|
||||
|
||||
@@ -1,82 +1,180 @@
|
||||
<h2 class="text-center">{%= __("Statement of Account") %}</h2>
|
||||
<h4 class="text-center">
|
||||
{% if (filters.party_name) { %}
|
||||
{%= filters.party_name %}
|
||||
{% } else if (filters.party && filters.party.length) { %}
|
||||
{%= filters.party %}
|
||||
{% } else if (filters.account) { %}
|
||||
{%= filters.account %}
|
||||
{% } %}
|
||||
</h4>
|
||||
<!-- Modified on 25-11-2024
|
||||
-->
|
||||
|
||||
<h6 class="text-center">
|
||||
{% if (filters.tax_id) { %}
|
||||
{%= __("Tax Id: ")%} {%= filters.tax_id %}
|
||||
{% } %}
|
||||
</h6>
|
||||
<style type="text/css">
|
||||
/* General styles for both screen display and print */
|
||||
body, html {
|
||||
margin-top: 10;
|
||||
padding: 0;
|
||||
width: 100%;
|
||||
height: auto; /* Allow content to expand */
|
||||
font-family: Arial, sans-serif; /* Example font */
|
||||
}
|
||||
|
||||
<h5 class="text-center">
|
||||
{%= frappe.datetime.str_to_user(filters.from_date) %}
|
||||
{%= __("to") %}
|
||||
{%= frappe.datetime.str_to_user(filters.to_date) %}
|
||||
</h5>
|
||||
<hr>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{%= __("Date") %}</th>
|
||||
<th style="width: 15%">{%= __("Reference") %}</th>
|
||||
<th style="width: 25%">{%= __("Remarks") %}</th>
|
||||
<th style="width: 15%">{%= __("Debit") %}</th>
|
||||
<th style="width: 15%">{%= __("Credit") %}</th>
|
||||
<th style="width: 18%">{%= __("Balance (Dr - Cr)") %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for(var i=0, l=data.length; i<l; i++) { %}
|
||||
<tr>
|
||||
{% if(data[i].posting_date) { %}
|
||||
<td>{%= frappe.datetime.str_to_user(data[i].posting_date) %}</td>
|
||||
<td>{%= data[i].voucher_type %}
|
||||
<br>{%= data[i].voucher_no %}
|
||||
</td>
|
||||
{% var longest_word = cstr(data[i].remarks).split(" ").reduce((longest, word) => word.length > longest.length ? word : longest, ""); %}
|
||||
<td {% if longest_word.length > 45 %} class="overflow-wrap-anywhere" {% endif %}>
|
||||
<span>
|
||||
{% if(!(filters.party || filters.account)) { %}
|
||||
{%= data[i].party || data[i].account %}
|
||||
<br>
|
||||
{% } %}
|
||||
/* Ensure consistent letter spacing across all media */
|
||||
.title-letter-spacing {
|
||||
letter-spacing: .2rem;
|
||||
}
|
||||
|
||||
{% if(data[i].remarks) { %}
|
||||
<br>{%= __("Remarks") %}: {%= data[i].remarks %}
|
||||
{% } else if(data[i].bill_no) { %}
|
||||
<br>{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</span>
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><b>{%= frappe.format(data[i].account, {fieldtype: "Link"}) || " " %}</b></td>
|
||||
<td style="text-align: right">
|
||||
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
{% } %}
|
||||
<td style="text-align: right">
|
||||
{%= format_currency(data[i].balance, filters.presentation_currency || data[i].account_currency) %}
|
||||
</td>
|
||||
/* Styles specific to printing and PDF generation */
|
||||
@media print {
|
||||
/* Set page size and margins for printing */
|
||||
@page {
|
||||
size: A4; /* Use fixed A4 page size */
|
||||
margin-top: 10mm;
|
||||
}
|
||||
|
||||
/* Force a page break before elements with the class "page-break" */
|
||||
.page-break {
|
||||
page-break-before: always;
|
||||
margin-top: 10mm; /* Add some space after the break */
|
||||
}
|
||||
|
||||
/* Ensure table headers repeat on each printed page */
|
||||
thead {
|
||||
display: table-header-group;
|
||||
}
|
||||
|
||||
/* Ensure table footers repeat on each printed page */
|
||||
tfoot {
|
||||
display: table-footer-group;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 1px;
|
||||
border: 1px solid black; /* Example border for clarity */
|
||||
}
|
||||
|
||||
/* Hide elements that should not appear in print (optional) */
|
||||
.no-print {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<br>
|
||||
<div style="font-family:Arial">
|
||||
<div>
|
||||
<div class="title-letter-spacing" style="text-align:center; font-size:15px; text-decoration:underline;">
|
||||
<b>
|
||||
{%= __("STATEMENT OF ACCOUNTS") %}<br>
|
||||
{% if (filters.party_name) { %}
|
||||
<br>{%= filters.party_name %}
|
||||
{% } else if (filters.party && filters.party.length) { %}
|
||||
<br>{%= filters.party %}
|
||||
{% } else if (filters.account) { %}
|
||||
<br>{%= filters.account %}
|
||||
{% } else { %}
|
||||
<br>{%= __("All Parties ") %}
|
||||
{% } %}
|
||||
</b>
|
||||
</div>
|
||||
<div style="text-align:center; font-size:13px;">
|
||||
<b>
|
||||
{% if(filters.party_type) { %}
|
||||
[ {%= filters.party_type %} ]<br>
|
||||
{% } %}
|
||||
{%= frappe.datetime.str_to_user(filters.from_date) %}
|
||||
{%= __("to") %}
|
||||
{%= frappe.datetime.str_to_user(filters.to_date) %}<br><br>
|
||||
</b>
|
||||
</div>
|
||||
</div>
|
||||
<table style="width:100%; font-size: 11px">
|
||||
<thead>
|
||||
<tr class="title-letter-spacing" style="text-align: center; font-weight:bold">
|
||||
<td style="border: 1.5px solid black; width: 7em">DATE</td>
|
||||
<td style="border: 1.5px solid black">PARTICULARS</td>
|
||||
{% if(filters.show_remarks) { %}
|
||||
<td style="border: 1.5px solid black">REMARKS</td>
|
||||
{% } %}
|
||||
<td style="border: 1.5px solid black; width: 9em">DEBIT</td>
|
||||
<td style="border: 1.5px solid black; width: 9em">CREDIT</td>
|
||||
<td style="border: 1.5px solid black; width: 10.2em">BALANCE</td>
|
||||
</tr>
|
||||
{% } %}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for(var i=0, l=data.length; i<l; i++) { %}
|
||||
<tr style="border-bottom: 1px solid black">
|
||||
{% if(data[i].posting_date) { %}
|
||||
<td style="text-align: center; border: 1px dotted black">
|
||||
{%= frappe.datetime.str_to_user(data[i].posting_date) %}
|
||||
</td>
|
||||
<td style="border-right: 1px dotted black">
|
||||
{%= data[i].voucher_type %} {%= data[i].voucher_no %}
|
||||
{% if(!(filters.party || filters.account)) { %}
|
||||
{%= data[i].party || data[i].account %}
|
||||
{% } %}<br>
|
||||
{% if(data[i].bill_no) { %}
|
||||
{%= __("Supplier Invoice No") %}: {%= data[i].bill_no %}
|
||||
{% } %}
|
||||
</td>
|
||||
{% if(filters.show_remarks) { %}
|
||||
<td style="border-right: 1px dotted black; font-size: 10px">
|
||||
{% if(data[i].remarks != "No Remarks" && data[i].remarks != "") { %}
|
||||
{%= __("Remarks") %}: {%= data[i].remarks %}<br>
|
||||
{% } %}
|
||||
</td>
|
||||
{% } %}
|
||||
<td style="text-align: right; border-right: 1px dotted black">
|
||||
{% if data[i].debit != 0 %}
|
||||
{%= format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: right; border-right: 1px dotted black">
|
||||
{% if data[i].credit != 0 %}
|
||||
{%= format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
{% } %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td style="text-align: center; border: 1px dotted black">
|
||||
{% if(i == 0) { %}
|
||||
{%= frappe.datetime.str_to_user(filters.from_date) %}
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: left; border-right: 1px dotted black"><b>
|
||||
{% if(i == l-2) { %}
|
||||
{%= "Total" %}
|
||||
{% } else { %}
|
||||
{% if(i == l-1) { %}
|
||||
{%= "Closing [Opening + Total] " %}
|
||||
{% } else { %}
|
||||
{%= frappe.format(data[i].account, {fieldtype: "Link"}) || " " %}
|
||||
{% } %}
|
||||
{% } %}</b>
|
||||
</td>
|
||||
{% if(filters.show_remarks) { %} <td style="text-align: left; border-right: 1px dotted black"></td>{% } %}
|
||||
<td style="text-align: right; border-right: 1px dotted black">
|
||||
{% if(i != 0){ %}
|
||||
{% if(i != l-1){ %}
|
||||
{%= data[i].account && format_currency(data[i].debit, filters.presentation_currency) %}
|
||||
{% } %}
|
||||
{% } %}
|
||||
</td>
|
||||
<td style="text-align: right; border-right: 1px dotted black">
|
||||
{% if(i != 0){ %}
|
||||
{% if(i != l-1){ %}
|
||||
{%= data[i].account && format_currency(data[i].credit, filters.presentation_currency) %}
|
||||
{% } %}
|
||||
{% } %}
|
||||
</td>
|
||||
{% } %}
|
||||
{% if(i == l-1) { %}
|
||||
<td style="text-align: right; font-weight:bold; border-right: 1px dotted black">
|
||||
{%= format_currency(data[i].balance, filters.presentation_currency) %}
|
||||
{% if(data[i].balance < 0){ %}Cr{% } %}
|
||||
{% if(data[i].balance > 0){ %}Dr{% } %}
|
||||
</td>
|
||||
{% } else { %}
|
||||
<td style="text-align: right; border-right: 1px dotted black">
|
||||
{% if(i != l-2) { %}
|
||||
{%= format_currency(data[i].balance, filters.presentation_currency) %}
|
||||
{% } %}
|
||||
</td>
|
||||
{% } %}
|
||||
</tr>
|
||||
{% endfor%}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="text-right text-muted">Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}</p>
|
||||
</div>
|
||||
|
||||
@@ -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()
|
||||
@@ -440,6 +440,7 @@ class GrossProfitGenerator:
|
||||
|
||||
if grouped_by_invoice:
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
for row in reversed(self.si_list):
|
||||
if self.filters.get("group_by") == "Monthly":
|
||||
@@ -480,12 +481,11 @@ class GrossProfitGenerator:
|
||||
else:
|
||||
row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision)
|
||||
|
||||
if grouped_by_invoice:
|
||||
if row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
elif row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
buying_amount = 0
|
||||
if grouped_by_invoice and row.indent == 0.0:
|
||||
row.buying_amount = buying_amount
|
||||
row.base_amount = base_amount
|
||||
buying_amount = 0
|
||||
base_amount = 0
|
||||
|
||||
# get buying rate
|
||||
if flt(row.qty):
|
||||
@@ -495,11 +495,19 @@ class GrossProfitGenerator:
|
||||
if self.is_not_invoice_row(row):
|
||||
row.buying_rate, row.base_rate = 0.0, 0.0
|
||||
|
||||
if self.is_not_invoice_row(row):
|
||||
self.update_return_invoices(row)
|
||||
|
||||
if grouped_by_invoice and row.indent == 1.0:
|
||||
buying_amount += row.buying_amount
|
||||
base_amount += row.base_amount
|
||||
|
||||
# calculate gross profit
|
||||
row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision)
|
||||
if row.base_amount:
|
||||
row.gross_profit_percent = flt(
|
||||
(row.gross_profit / row.base_amount) * 100.0, self.currency_precision
|
||||
(row.gross_profit / row.base_amount) * 100.0,
|
||||
self.currency_precision,
|
||||
)
|
||||
else:
|
||||
row.gross_profit_percent = 0.0
|
||||
@@ -510,33 +518,29 @@ class GrossProfitGenerator:
|
||||
if self.grouped:
|
||||
self.get_average_rate_based_on_group_by()
|
||||
|
||||
def update_return_invoices(self, row):
|
||||
if row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent]:
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
returned_item_row.qty = 0
|
||||
returned_item_row.base_amount = 0
|
||||
|
||||
else:
|
||||
row.qty = 0
|
||||
row.base_amount = 0
|
||||
returned_item_row.qty += row.qty
|
||||
returned_item_row.base_amount += row.base_amount
|
||||
|
||||
row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision)
|
||||
|
||||
def get_average_rate_based_on_group_by(self):
|
||||
for key in list(self.grouped):
|
||||
if self.filters.get("group_by") == "Invoice":
|
||||
for row in self.grouped[key]:
|
||||
if row.indent == 1.0:
|
||||
if (
|
||||
row.parent in self.returned_invoices
|
||||
and row.item_code in self.returned_invoices[row.parent]
|
||||
):
|
||||
returned_item_rows = self.returned_invoices[row.parent][row.item_code]
|
||||
for returned_item_row in returned_item_rows:
|
||||
# returned_items 'qty' should be stateful
|
||||
if returned_item_row.qty != 0:
|
||||
if row.qty >= abs(returned_item_row.qty):
|
||||
row.qty += returned_item_row.qty
|
||||
returned_item_row.qty = 0
|
||||
else:
|
||||
row.qty = 0
|
||||
returned_item_row.qty += row.qty
|
||||
row.base_amount += flt(returned_item_row.base_amount, self.currency_precision)
|
||||
row.buying_amount = flt(
|
||||
flt(row.qty) * flt(row.buying_rate), self.currency_precision
|
||||
)
|
||||
if flt(row.qty) or row.base_amount:
|
||||
row = self.set_average_rate(row)
|
||||
self.grouped_data.append(row)
|
||||
elif self.filters.get("group_by") == "Payment Term":
|
||||
if self.filters.get("group_by") == "Payment Term":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
invoice_portion = 0
|
||||
|
||||
@@ -556,7 +560,7 @@ class GrossProfitGenerator:
|
||||
|
||||
new_row = self.set_average_rate(new_row)
|
||||
self.grouped_data.append(new_row)
|
||||
else:
|
||||
elif self.filters.get("group_by") != "Invoice":
|
||||
for i, row in enumerate(self.grouped[key]):
|
||||
if i == 0:
|
||||
new_row = row
|
||||
@@ -632,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)
|
||||
@@ -664,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
|
||||
|
||||
@@ -847,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,
|
||||
@@ -907,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
|
||||
@@ -917,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)
|
||||
@@ -954,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:
|
||||
|
||||
@@ -421,12 +421,12 @@ class TestGrossProfit(IntegrationTestCase):
|
||||
"item_name": self.item,
|
||||
"warehouse": "Stores - _GP",
|
||||
"qty": 0.0,
|
||||
"avg._selling_rate": 0.0,
|
||||
"avg._selling_rate": 100,
|
||||
"valuation_rate": 0.0,
|
||||
"selling_amount": -100.0,
|
||||
"selling_amount": 0.0,
|
||||
"buying_amount": 0.0,
|
||||
"gross_profit": -100.0,
|
||||
"gross_profit_%": 100.0,
|
||||
"gross_profit": 0.0,
|
||||
"gross_profit_%": 0.0,
|
||||
}
|
||||
gp_entry = [x for x in data if x.parent_invoice == sinv.name]
|
||||
# Both items of Invoice should have '0' qty
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -93,14 +93,14 @@ class TestUtils(IntegrationTestCase):
|
||||
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)
|
||||
|
||||
@@ -416,7 +416,7 @@ frappe.ui.form.on("Asset", {
|
||||
}
|
||||
|
||||
frm.dashboard.render_graph({
|
||||
title: "Asset Value",
|
||||
title: __("Asset Value"),
|
||||
data: {
|
||||
labels: x_intervals,
|
||||
datasets: [
|
||||
|
||||
@@ -8,29 +8,20 @@
|
||||
"document_type": "Document",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"naming_series",
|
||||
"item_code",
|
||||
"item_name",
|
||||
"asset_name",
|
||||
"asset_category",
|
||||
"location",
|
||||
"image",
|
||||
"column_break_3",
|
||||
"status",
|
||||
"company",
|
||||
"asset_owner",
|
||||
"asset_owner_company",
|
||||
"is_existing_asset",
|
||||
"is_composite_asset",
|
||||
"supplier",
|
||||
"customer",
|
||||
"image",
|
||||
"journal_entry_for_scrap",
|
||||
"column_break_3",
|
||||
"naming_series",
|
||||
"asset_name",
|
||||
"asset_category",
|
||||
"location",
|
||||
"split_from",
|
||||
"custodian",
|
||||
"department",
|
||||
"disposal_date",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"dimension_col_break",
|
||||
"purchase_details_section",
|
||||
"purchase_receipt",
|
||||
"purchase_receipt_item",
|
||||
@@ -40,10 +31,12 @@
|
||||
"available_for_use_date",
|
||||
"column_break_23",
|
||||
"gross_purchase_amount",
|
||||
"purchase_amount",
|
||||
"asset_quantity",
|
||||
"additional_asset_cost",
|
||||
"total_asset_cost",
|
||||
"section_break_23",
|
||||
"disposal_date",
|
||||
"depreciation_tab",
|
||||
"calculate_depreciation",
|
||||
"column_break_33",
|
||||
"opening_accumulated_depreciation",
|
||||
@@ -60,7 +53,7 @@
|
||||
"next_depreciation_date",
|
||||
"depreciation_schedule_sb",
|
||||
"depreciation_schedule_view",
|
||||
"insurance_details",
|
||||
"insurance_details_tab",
|
||||
"policy_number",
|
||||
"insurer",
|
||||
"insured_value",
|
||||
@@ -68,22 +61,29 @@
|
||||
"insurance_start_date",
|
||||
"insurance_end_date",
|
||||
"comprehensive_insurance",
|
||||
"section_break_31",
|
||||
"maintenance_required",
|
||||
"other_details",
|
||||
"status",
|
||||
"booked_fixed_asset",
|
||||
"column_break_51",
|
||||
"purchase_amount",
|
||||
"other_info_tab",
|
||||
"accounting_dimensions_section",
|
||||
"cost_center",
|
||||
"section_break_jtou",
|
||||
"custodian",
|
||||
"default_finance_book",
|
||||
"depr_entry_posting_status",
|
||||
"amended_from"
|
||||
"booked_fixed_asset",
|
||||
"customer",
|
||||
"supplier",
|
||||
"column_break_51",
|
||||
"department",
|
||||
"split_from",
|
||||
"journal_entry_for_scrap",
|
||||
"amended_from",
|
||||
"maintenance_required",
|
||||
"connections_tab"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "naming_series",
|
||||
"fieldtype": "Select",
|
||||
"label": "Naming Series",
|
||||
"label": "Series",
|
||||
"options": "ACC-ASS-.YYYY.-"
|
||||
},
|
||||
{
|
||||
@@ -124,6 +124,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "Company",
|
||||
"fieldname": "asset_owner",
|
||||
"fieldtype": "Select",
|
||||
"label": "Asset Owner",
|
||||
@@ -257,18 +258,10 @@
|
||||
"label": "Opening Accumulated Depreciation",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.calculate_depreciation || doc.is_existing_asset",
|
||||
"fieldname": "section_break_23",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Depreciation"
|
||||
},
|
||||
{
|
||||
"columns": 10,
|
||||
"fieldname": "finance_books",
|
||||
"fieldtype": "Table",
|
||||
"label": "Finance Books",
|
||||
"options": "Asset Finance Book"
|
||||
},
|
||||
{
|
||||
@@ -310,12 +303,6 @@
|
||||
"label": "Next Depreciation Date",
|
||||
"no_copy": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "insurance_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Insurance details"
|
||||
},
|
||||
{
|
||||
"fieldname": "policy_number",
|
||||
"fieldtype": "Data",
|
||||
@@ -350,25 +337,13 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Comprehensive Insurance"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_31",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Maintenance"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "0",
|
||||
"description": "Check if Asset requires Preventive Maintenance or Calibration",
|
||||
"fieldname": "maintenance_required",
|
||||
"fieldtype": "Check",
|
||||
"label": "Maintenance Required"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "other_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Other Details"
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "Draft",
|
||||
@@ -385,6 +360,7 @@
|
||||
"default": "0",
|
||||
"fieldname": "booked_fixed_asset",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Booked Fixed Asset",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
@@ -428,16 +404,6 @@
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "is_existing_asset",
|
||||
@@ -552,6 +518,37 @@
|
||||
"hidden": 1,
|
||||
"label": "Purchase Invoice Item",
|
||||
"options": "Purchase Invoice Item"
|
||||
},
|
||||
{
|
||||
"fieldname": "insurance_details_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Insurance"
|
||||
},
|
||||
{
|
||||
"fieldname": "other_info_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Other Info"
|
||||
},
|
||||
{
|
||||
"fieldname": "connections_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Connections",
|
||||
"show_dashboard": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "depreciation_tab",
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Depreciation"
|
||||
},
|
||||
{
|
||||
"fieldname": "accounting_dimensions_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Dimensions"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jtou",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Additional Info"
|
||||
}
|
||||
],
|
||||
"idx": 72,
|
||||
@@ -595,7 +592,7 @@
|
||||
"link_fieldname": "target_asset"
|
||||
}
|
||||
],
|
||||
"modified": "2024-08-26 23:28:29.095139",
|
||||
"modified": "2024-11-29 14:25:56.436124",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset",
|
||||
|
||||
@@ -322,7 +322,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_stock_item_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
ctx: {
|
||||
item_code: row.item_code,
|
||||
warehouse: row.warehouse,
|
||||
stock_qty: flt(row.stock_qty),
|
||||
@@ -350,7 +350,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_consumed_asset_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
ctx: {
|
||||
asset: row.asset,
|
||||
doctype: me.frm.doc.doctype,
|
||||
name: me.frm.doc.name,
|
||||
@@ -377,7 +377,7 @@ erpnext.assets.AssetCapitalization = class AssetCapitalization extends erpnext.s
|
||||
method: "erpnext.assets.doctype.asset_capitalization.asset_capitalization.get_service_item_details",
|
||||
child: row,
|
||||
args: {
|
||||
args: {
|
||||
ctx: {
|
||||
item_code: row.item_code,
|
||||
qty: flt(row.qty),
|
||||
expense_account: row.expense_account,
|
||||
|
||||
@@ -7,17 +7,19 @@
|
||||
"field_order": [
|
||||
"finance_book",
|
||||
"depreciation_method",
|
||||
"total_number_of_depreciations",
|
||||
"total_number_of_booked_depreciations",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"column_break_5",
|
||||
"frequency_of_depreciation",
|
||||
"total_number_of_depreciations",
|
||||
"depreciation_start_date",
|
||||
"column_break_5",
|
||||
"salvage_value_percentage",
|
||||
"expected_value_after_useful_life",
|
||||
"rate_of_depreciation",
|
||||
"daily_prorata_based",
|
||||
"shift_based",
|
||||
"section_break_jkdf",
|
||||
"value_after_depreciation",
|
||||
"rate_of_depreciation"
|
||||
"column_break_sigk",
|
||||
"total_number_of_booked_depreciations"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -67,9 +69,10 @@
|
||||
"columns": 1,
|
||||
"default": "0",
|
||||
"depends_on": "eval:parent.doctype == 'Asset'",
|
||||
"description": "Expected Value After Useful Life",
|
||||
"fieldname": "expected_value_after_useful_life",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Expected Value After Useful Life",
|
||||
"label": "Salvage Value",
|
||||
"options": "Company:company:default_currency"
|
||||
},
|
||||
{
|
||||
@@ -83,10 +86,9 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.depreciation_method == 'Written Down Value'",
|
||||
"description": "In Percentage",
|
||||
"fieldname": "rate_of_depreciation",
|
||||
"fieldtype": "Percent",
|
||||
"label": "Rate of Depreciation"
|
||||
"label": "Rate of Depreciation (%)"
|
||||
},
|
||||
{
|
||||
"fieldname": "salvage_value_percentage",
|
||||
@@ -108,16 +110,25 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "total_number_of_booked_depreciations",
|
||||
"fieldname": "total_number_of_booked_depreciations",
|
||||
"fieldtype": "Int",
|
||||
"label": "Total Number of Booked Depreciations ",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_jkdf",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_sigk",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-05-21 15:48:20.907250",
|
||||
"modified": "2024-11-29 14:36:54.399034",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Assets",
|
||||
"name": "Asset Finance Book",
|
||||
|
||||
@@ -93,10 +93,7 @@ frappe.ui.form.on("Purchase Order", {
|
||||
get_materials_from_supplier: function (frm) {
|
||||
let po_details = [];
|
||||
|
||||
if (
|
||||
frm.doc.supplied_items &&
|
||||
(flt(frm.doc.per_received, precision("per_received")) == 100 || frm.doc.status === "Closed")
|
||||
) {
|
||||
if (frm.doc.supplied_items && (flt(frm.doc.per_received) == 100 || frm.doc.status === "Closed")) {
|
||||
frm.doc.supplied_items.forEach((d) => {
|
||||
if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
|
||||
po_details.push(d.name);
|
||||
@@ -332,8 +329,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
if (!["Closed", "Delivered"].includes(doc.status)) {
|
||||
if (
|
||||
this.frm.doc.status !== "Closed" &&
|
||||
flt(this.frm.doc.per_received, precision("per_received")) < 100 &&
|
||||
flt(this.frm.doc.per_billed, precision("per_billed")) < 100
|
||||
flt(this.frm.doc.per_received) < 100 &&
|
||||
flt(this.frm.doc.per_billed) < 100
|
||||
) {
|
||||
if (!this.frm.doc.__onload || this.frm.doc.__onload.can_update_items) {
|
||||
this.frm.add_custom_button(__("Update Items"), () => {
|
||||
@@ -347,10 +344,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
}
|
||||
if (this.frm.has_perm("submit")) {
|
||||
if (
|
||||
flt(doc.per_billed, precision("per_billed")) < 100 ||
|
||||
flt(doc.per_received, precision("per_received")) < 100
|
||||
) {
|
||||
if (flt(doc.per_billed) < 100 || flt(doc.per_received) < 100) {
|
||||
if (doc.status != "On Hold") {
|
||||
this.frm.add_custom_button(
|
||||
__("Hold"),
|
||||
@@ -388,7 +382,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
if (doc.status != "Closed") {
|
||||
if (doc.status != "On Hold") {
|
||||
if (flt(doc.per_received, precision("per_received")) < 100 && allow_receipt) {
|
||||
if (flt(doc.per_received) < 100 && allow_receipt) {
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Receipt"),
|
||||
() => {
|
||||
@@ -419,7 +413,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
}
|
||||
}
|
||||
// Please do not add precision in the below flt function
|
||||
if (flt(doc.per_billed, precision("per_billed")) < 100)
|
||||
if (flt(doc.per_billed) < 100)
|
||||
this.frm.add_custom_button(
|
||||
__("Purchase Invoice"),
|
||||
() => {
|
||||
@@ -428,7 +422,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
__("Create")
|
||||
);
|
||||
|
||||
if (flt(doc.per_billed, precision("per_billed")) < 100 && doc.status != "Delivered") {
|
||||
if (flt(doc.per_billed) < 100 && doc.status != "Delivered") {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment"),
|
||||
() => this.make_payment_entry(),
|
||||
@@ -436,7 +430,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
);
|
||||
}
|
||||
|
||||
if (flt(doc.per_billed, precision("per_billed")) < 100) {
|
||||
if (flt(doc.per_billed) < 100) {
|
||||
this.frm.add_custom_button(
|
||||
__("Payment Request"),
|
||||
function () {
|
||||
|
||||
@@ -2493,6 +2493,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"
|
||||
@@ -2517,7 +2523,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:
|
||||
@@ -2526,17 +2532,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"]
|
||||
|
||||
@@ -1036,7 +1036,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
available_serial_nos.append(serial_no)
|
||||
|
||||
if available_serial_nos:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
available_serial_nos = get_available_serial_nos(available_serial_nos, warehouse)
|
||||
|
||||
if len(available_serial_nos) > qty:
|
||||
@@ -1052,7 +1052,7 @@ def filter_serial_batches(parent_doc, data, row, warehouse_field=None, qty_field
|
||||
if batch_qty <= 0:
|
||||
continue
|
||||
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Reecipt"]:
|
||||
if parent_doc.doctype in ["Purchase Invoice", "Purchase Receipt"]:
|
||||
batch_qty = get_available_batch_qty(
|
||||
parent_doc,
|
||||
batch_no,
|
||||
|
||||
@@ -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"),
|
||||
@@ -378,12 +372,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(
|
||||
|
||||
@@ -809,6 +809,7 @@ class TestAccountsController(IntegrationTestCase):
|
||||
"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()
|
||||
@@ -842,6 +843,31 @@ class TestAccountsController(IntegrationTestCase):
|
||||
# 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)
|
||||
|
||||
1539
erpnext/locale/ar.po
1539
erpnext/locale/ar.po
File diff suppressed because it is too large
Load Diff
1537
erpnext/locale/bs.po
1537
erpnext/locale/bs.po
File diff suppressed because it is too large
Load Diff
1541
erpnext/locale/de.po
1541
erpnext/locale/de.po
File diff suppressed because it is too large
Load Diff
1541
erpnext/locale/eo.po
1541
erpnext/locale/eo.po
File diff suppressed because it is too large
Load Diff
1583
erpnext/locale/es.po
1583
erpnext/locale/es.po
File diff suppressed because it is too large
Load Diff
1615
erpnext/locale/fa.po
1615
erpnext/locale/fa.po
File diff suppressed because it is too large
Load Diff
1539
erpnext/locale/fr.po
1539
erpnext/locale/fr.po
File diff suppressed because it is too large
Load Diff
1539
erpnext/locale/hu.po
1539
erpnext/locale/hu.po
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
1537
erpnext/locale/pl.po
1537
erpnext/locale/pl.po
File diff suppressed because it is too large
Load Diff
1541
erpnext/locale/ru.po
1541
erpnext/locale/ru.po
File diff suppressed because it is too large
Load Diff
1655
erpnext/locale/sv.po
1655
erpnext/locale/sv.po
File diff suppressed because it is too large
Load Diff
1711
erpnext/locale/tr.po
1711
erpnext/locale/tr.po
File diff suppressed because it is too large
Load Diff
1541
erpnext/locale/zh.po
1541
erpnext/locale/zh.po
File diff suppressed because it is too large
Load Diff
@@ -264,6 +264,24 @@ class BOM(WebsiteGenerator):
|
||||
self.update_cost(update_parent=False, from_child_bom=True, update_hour_rate=False, save=False)
|
||||
self.set_process_loss_qty()
|
||||
self.validate_scrap_items()
|
||||
self.set_default_uom()
|
||||
|
||||
def set_default_uom(self):
|
||||
if not self.get("items"):
|
||||
return
|
||||
|
||||
item_wise_uom = frappe._dict(
|
||||
frappe.get_all(
|
||||
"Item",
|
||||
filters={"name": ("in", [item.item_code for item in self.items])},
|
||||
fields=["name", "stock_uom"],
|
||||
as_list=1,
|
||||
)
|
||||
)
|
||||
|
||||
for row in self.get("items"):
|
||||
if row.stock_uom != item_wise_uom.get(row.item_code):
|
||||
row.stock_uom = item_wise_uom.get(row.item_code)
|
||||
|
||||
def get_context(self, context):
|
||||
context.parents = [{"name": "boms", "title": _("All BOMs")}]
|
||||
|
||||
@@ -763,6 +763,26 @@ class TestBOM(IntegrationTestCase):
|
||||
self.assertTrue("_Test RM Item 2 Fixed Asset Item" not in items)
|
||||
self.assertTrue("_Test RM Item 3 Manufacture Item" in items)
|
||||
|
||||
def test_bom_raw_materials_stock_uom(self):
|
||||
rm_item = make_item(
|
||||
properties={"is_stock_item": 1, "valuation_rate": 1000.0, "stock_uom": "Nos"}
|
||||
).name
|
||||
fg_item = make_item(properties={"is_stock_item": 1}).name
|
||||
|
||||
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
|
||||
|
||||
bom = make_bom(item=fg_item, raw_materials=[rm_item], do_not_submit=True)
|
||||
for row in bom.items:
|
||||
self.assertEqual(row.stock_uom, "Nos")
|
||||
|
||||
frappe.db.set_value("Item", rm_item, "stock_uom", "Kg")
|
||||
|
||||
bom.items[0].qty = 2
|
||||
bom.save()
|
||||
|
||||
for row in bom.items:
|
||||
self.assertEqual(row.stock_uom, "Kg")
|
||||
|
||||
|
||||
def get_default_bom(item_code="_Test FG Item 2"):
|
||||
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
|
||||
|
||||
@@ -243,7 +243,7 @@
|
||||
"depends_on": "eval:!doc.__islocal",
|
||||
"fieldname": "download_materials_required",
|
||||
"fieldtype": "Button",
|
||||
"label": "Download Materials Request Plan"
|
||||
"label": "Download Required Materials"
|
||||
},
|
||||
{
|
||||
"fieldname": "get_items_for_mr",
|
||||
@@ -398,7 +398,7 @@
|
||||
"collapsible": 1,
|
||||
"fieldname": "download_materials_request_plan_section_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Download Materials Request Plan Section"
|
||||
"label": "Preview Required Materials"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
@@ -439,7 +439,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2024-03-27 13:10:19.928403",
|
||||
"modified": "2024-12-04 11:55:03.108971",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Manufacturing",
|
||||
"name": "Production Plan",
|
||||
|
||||
@@ -44,9 +44,7 @@ class ProductionPlan(Document):
|
||||
from erpnext.manufacturing.doctype.material_request_plan_item.material_request_plan_item import (
|
||||
MaterialRequestPlanItem,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import (
|
||||
ProductionPlanItem,
|
||||
)
|
||||
from erpnext.manufacturing.doctype.production_plan_item.production_plan_item import ProductionPlanItem
|
||||
from erpnext.manufacturing.doctype.production_plan_item_reference.production_plan_item_reference import (
|
||||
ProductionPlanItemReference,
|
||||
)
|
||||
@@ -1085,24 +1083,33 @@ def download_raw_materials(doc, warehouses=None):
|
||||
frappe.flags.show_qty_in_stock_uom = 1
|
||||
items = get_items_for_material_requests(doc, warehouses=warehouses, get_parent_warehouse_data=True)
|
||||
|
||||
duplicate_item_wh_list = frappe._dict()
|
||||
|
||||
for d in items:
|
||||
item_list.append(
|
||||
[
|
||||
d.get("item_code"),
|
||||
d.get("item_name"),
|
||||
d.get("description"),
|
||||
d.get("stock_uom"),
|
||||
d.get("warehouse"),
|
||||
d.get("required_bom_qty"),
|
||||
d.get("projected_qty"),
|
||||
d.get("actual_qty"),
|
||||
d.get("ordered_qty"),
|
||||
d.get("planned_qty"),
|
||||
d.get("reserved_qty_for_production"),
|
||||
d.get("safety_stock"),
|
||||
d.get("quantity"),
|
||||
]
|
||||
)
|
||||
key = (d.get("item_code"), d.get("warehouse"))
|
||||
if key in duplicate_item_wh_list:
|
||||
rm_data = duplicate_item_wh_list[key]
|
||||
rm_data[12] += d.get("quantity")
|
||||
continue
|
||||
|
||||
rm_data = [
|
||||
d.get("item_code"),
|
||||
d.get("item_name"),
|
||||
d.get("description"),
|
||||
d.get("stock_uom"),
|
||||
d.get("warehouse"),
|
||||
d.get("required_bom_qty"),
|
||||
d.get("projected_qty"),
|
||||
d.get("actual_qty"),
|
||||
d.get("ordered_qty"),
|
||||
d.get("planned_qty"),
|
||||
d.get("reserved_qty_for_production"),
|
||||
d.get("safety_stock"),
|
||||
d.get("quantity"),
|
||||
]
|
||||
|
||||
duplicate_item_wh_list[key] = rm_data
|
||||
item_list.append(rm_data)
|
||||
|
||||
if not doc.get("for_warehouse"):
|
||||
row = {"item_code": d.get("item_code")}
|
||||
|
||||
@@ -178,10 +178,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
|
||||
|
||||
@@ -389,4 +389,5 @@ erpnext.patches.v15_0.update_sub_voucher_type_in_gl_entries
|
||||
erpnext.patches.v15_0.update_task_assignee_email_field_in_asset_maintenance_log
|
||||
erpnext.patches.v14_0.update_currency_exchange_settings_for_frankfurter
|
||||
erpnext.patches.v15_0.migrate_old_item_wise_tax_detail_data_format
|
||||
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
erpnext.patches.v15_0.set_is_exchange_gain_loss_in_payment_entry_deductions
|
||||
erpnext.patches.v14_0.update_stock_uom_in_work_order_item
|
||||
@@ -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()
|
||||
@@ -58,10 +58,10 @@ frappe.ui.form.on("Timesheet", {
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus < 1) {
|
||||
let button = "Start Timer";
|
||||
let button = __("Start Timer");
|
||||
$.each(frm.doc.time_logs || [], function (i, row) {
|
||||
if (row.from_time <= frappe.datetime.now_datetime() && !row.completed) {
|
||||
button = "Resume Timer";
|
||||
button = __("Resume Timer");
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user