chore: release v14 (#37654)

* fix(delivery): rename dt fetch stop action (backport #37605) (#37606)

fix(delivery): rename dt fetch stop action

(cherry picked from commit 79d51a0a0b)

Co-authored-by: David Arnold <dgx.arnold@gmail.com>

* fix: incorrect cost center in the purchase invoice (backport #37591) (#37609)

* fix: incorrect cost center in the purchase invoice (#37591)

(cherry picked from commit 14b009b093)

# Conflicts:
#	erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py

* chore: fix conflicts

---------

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>

* fix(minor): filter bank accounts in bank statement import (#37525)

fix(minor): filter bank accounts in bank statement import (#37525)

fix: filter by company in bank account
(cherry picked from commit 9d392970f0)

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>

* fix: set empty value for tax template in item details (#37496)

* fix: set empty value for tax template in item details (#37496)

* fix: empty tax template for items with invalid templates

* fix: test for empty tax template

* fix: test for item tax template calculation

* fix: test for pos inv tax template calculation

(cherry picked from commit b0d440c34b)

# Conflicts:
#	erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py

* chore: resolve conflicts

---------

Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>

* fix(minor): filter tax template based on company in subscription (#37562)

fix: filter tax template based on company

(cherry picked from commit 1a2f659de2)

Co-authored-by: Gursheen Anand <gursheen@frappe.io>

* fix: Cash flow mapping fix (#37522)

Cash flow mapping fix

* fix: remove from or target warehouse for non internal transfer entries (backport #37612) (#37628)

fix: remove from or target warehouse for non internal transfer entries (#37612)

(cherry picked from commit 5136fe196b)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>

* Revert "fix: set empty value for tax template in item details (#37496)"

Revert "fix: set empty value for tax template in item details (#37496)"

This reverts commit ec208b8df5.

* refactor: gain_loss posting date fields in the allocation table

(cherry picked from commit 55dbcee36a)

* refactor: introduce fields in popup

(cherry picked from commit 5323bb7bee)

* refactor: pass gain loss posting date to controller

(cherry picked from commit 7e600a6494)

* test: varying posting date for gain loss journal

(cherry picked from commit 514d5434a3)

* fix: overallocation on Payment with PO/SO

(cherry picked from commit 23df4205f8)

# Conflicts:
#	erpnext/accounts/utils.py

* test: overalloction on reconciliation when PO is involved

(cherry picked from commit 946228d783)

* refactor(test): make use of utility methods

(cherry picked from commit 547993f801)

* chore: fix flakiness `test_sales_order_partial_advance_payment`

(cherry picked from commit 4dff2c7a0d)

* chore: resolve conflict

* fix: close employee loan on write off (#37638)

* fix: exclude written off amount while calculating loan repayment

* fix: revert exclude written off amount while calculating loan repayment

* fix: close employee loan on write off

* refactor: button on PE to filter associated Journals

(cherry picked from commit 150728deaa)

# Conflicts:
#	erpnext/accounts/doctype/payment_entry/payment_entry.js

* chore: resolve conflict

* fix: incorrect process loss validation for multiple finished items (backport #37576) (#37656)

fix: incorrect process loss validation for multiple finished items (#37576)

(cherry picked from commit 92cbe580e6)

Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>

* chore: fixed test cases related to Internal Transfer (#37659)

* fix: GL Entries for receiving non CWIP assets using Purchase Receipt (#37660)

* fix: GL Entries for receiving non CWIP assets using Purchase Receipt

* fix: rearrange functions

* chore: rearrange functions

* chore: rearrange functions

* fix: Purchase Invoice GL entires for assets

* test: cwip accounting unit tests

* chore: Attribute error

* chore: Purchase Invoice tests

* chore: Missing asset account

* chore: Missing asset account

* chore: update tests

* fix: Internal transfer GL Entries

* test: Deprecate tests

* test: Depricate tests

* test: Depricate tests

* chore: make `Reserve Stock` checkbox visible in SO

* refactor: rename field `Auto Reserve Stock for Sales Order`

* feat: add fields to hold SO and SO Item ref in PR Item

* test: Deprecate tests

* test: Depricate tests

* test: Depricate tests

* refactor: Remove expense included in valuation accounts

* chore: Add back default in transit warehousefield

---------

Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>

* fix: wrong german translation (#37658)

---------

Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
Co-authored-by: David Arnold <dgx.arnold@gmail.com>
Co-authored-by: rohitwaghchaure <rohitw1991@gmail.com>
Co-authored-by: Gursheen Kaur Anand <40693548+GursheenK@users.noreply.github.com>
Co-authored-by: Gursheen Anand <gursheen@frappe.io>
Co-authored-by: saeedkola <mohammedsaeedk@gmail.com>
Co-authored-by: Deepesh Garg <deepeshgarg6@gmail.com>
Co-authored-by: ruthra kumar <ruthra@erpnext.com>
Co-authored-by: Anand Baburajan <anandbaburajan@gmail.com>
Co-authored-by: s-aga-r <sagarsharma.s312@gmail.com>
Co-authored-by: Raffael Meyer <14891507+barredterra@users.noreply.github.com>
This commit is contained in:
Frappe PR Bot
2023-10-25 12:03:24 +05:30
committed by GitHub
parent 6931db98f1
commit f35a0c227d
39 changed files with 939 additions and 746 deletions

View File

@@ -2,6 +2,16 @@
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Bank Statement Import", { frappe.ui.form.on("Bank Statement Import", {
onload(frm) {
frm.set_query("bank_account", function (doc) {
return {
filters: {
company: doc.company,
},
};
});
},
setup(frm) { setup(frm) {
frappe.realtime.on("data_import_refresh", ({ data_import }) => { frappe.realtime.on("data_import_refresh", ({ data_import }) => {
frm.import_in_progress = false; frm.import_in_progress = false;

View File

@@ -152,6 +152,12 @@ frappe.ui.form.on('Payment Entry', {
frm.events.hide_unhide_fields(frm); frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm); frm.events.set_dynamic_labels(frm);
frm.events.show_general_ledger(frm); frm.events.show_general_ledger(frm);
if(frm.doc.references.find((elem) => {return elem.exchange_gain_loss != 0})) {
frm.add_custom_button(__("View Exchange Gain/Loss Journals"), function() {
frappe.set_route("List", "Journal Entry", {"voucher_type": "Exchange Gain Or Loss", "reference_name": frm.doc.name});
}, __('Actions'));
}
erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm); erpnext.accounts.unreconcile_payments.add_unreconcile_btn(frm);
}, },

View File

@@ -216,6 +216,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
this.data = []; this.data = [];
const dialog = new frappe.ui.Dialog({ const dialog = new frappe.ui.Dialog({
title: __("Select Difference Account"), title: __("Select Difference Account"),
size: 'extra-large',
fields: [ fields: [
{ {
fieldname: "allocation", fieldname: "allocation",
@@ -239,6 +240,13 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
in_list_view: 1, in_list_view: 1,
read_only: 1 read_only: 1
}, { }, {
fieldtype:'Date',
fieldname:"gain_loss_posting_date",
label: __("Posting Date"),
in_list_view: 1,
reqd: 1,
}, {
fieldtype:'Link', fieldtype:'Link',
options: 'Account', options: 'Account',
in_list_view: 1, in_list_view: 1,
@@ -272,6 +280,9 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
args.forEach(d => { args.forEach(d => {
frappe.model.set_value("Payment Reconciliation Allocation", d.docname, frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
"difference_account", d.difference_account); "difference_account", d.difference_account);
frappe.model.set_value("Payment Reconciliation Allocation", d.docname,
"gain_loss_posting_date", d.gain_loss_posting_date);
}); });
this.reconcile_payment_entries(); this.reconcile_payment_entries();
@@ -287,6 +298,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
'reference_name': d.reference_name, 'reference_name': d.reference_name,
'difference_amount': d.difference_amount, 'difference_amount': d.difference_amount,
'difference_account': d.difference_account, 'difference_account': d.difference_account,
'gain_loss_posting_date': d.gain_loss_posting_date
}); });
} }
}); });

View File

@@ -315,6 +315,7 @@ class PaymentReconciliation(Document):
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"]) res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate") res.exchange_rate = inv.get("exchange_rate")
res.update({"gain_loss_posting_date": pay.get("posting_date")})
if pay.get("amount") == 0: if pay.get("amount") == 0:
entries.append(res) entries.append(res)
@@ -421,6 +422,7 @@ class PaymentReconciliation(Document):
"allocated_amount": flt(row.get("allocated_amount")), "allocated_amount": flt(row.get("allocated_amount")),
"difference_amount": flt(row.get("difference_amount")), "difference_amount": flt(row.get("difference_amount")),
"difference_account": row.get("difference_account"), "difference_account": row.get("difference_account"),
"difference_posting_date": row.get("gain_loss_posting_date"),
"cost_center": row.get("cost_center"), "cost_center": row.get("cost_center"),
} }
) )

View File

@@ -14,6 +14,7 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_pay
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account from erpnext.accounts.party import get_party_account
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.item.test_item import create_item
test_dependencies = ["Item"] test_dependencies = ["Item"]
@@ -85,26 +86,44 @@ class TestPaymentReconciliation(FrappeTestCase):
self.customer5 = make_customer("_Test PR Customer 5", "EUR") self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self): def create_account(self):
account_name = "Debtors EUR" accounts = [
if not frappe.db.get_value( {
"Account", filters={"account_name": account_name, "company": self.company} "attribute": "debtors_eur",
): "account_name": "Debtors EUR",
acc = frappe.new_doc("Account") "parent_account": "Accounts Receivable - _PR",
acc.account_name = account_name "account_currency": "EUR",
acc.parent_account = "Accounts Receivable - _PR" "account_type": "Receivable",
acc.company = self.company },
acc.account_currency = "EUR" {
acc.account_type = "Receivable" "attribute": "creditors_usd",
acc.insert() "account_name": "Payable USD",
else: "parent_account": "Accounts Payable - _PR",
name = frappe.db.get_value( "account_currency": "USD",
"Account", "account_type": "Payable",
filters={"account_name": account_name, "company": self.company}, },
fieldname="name", ]
pluck=True,
) for x in accounts:
acc = frappe.get_doc("Account", name) x = frappe._dict(x)
self.debtors_eur = acc.name if not frappe.db.get_value(
"Account", filters={"account_name": x.account_name, "company": self.company}
):
acc = frappe.new_doc("Account")
acc.account_name = x.account_name
acc.parent_account = x.parent_account
acc.company = self.company
acc.account_currency = x.account_currency
acc.account_type = x.account_type
acc.insert()
else:
name = frappe.db.get_value(
"Account",
filters={"account_name": x.account_name, "company": self.company},
fieldname="name",
pluck=True,
)
acc = frappe.get_doc("Account", name)
setattr(self, x.attribute, acc.name)
def create_sales_invoice( def create_sales_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
@@ -151,6 +170,64 @@ class TestPaymentReconciliation(FrappeTestCase):
payment.posting_date = posting_date payment.posting_date = posting_date
return payment return payment
def create_purchase_invoice(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
pinv = make_purchase_invoice(
qty=qty,
rate=rate,
company=self.company,
customer=self.supplier,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return pinv
def create_purchase_order(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
"""
Helper function to populate default values in sales invoice
"""
pord = create_purchase_order(
qty=qty,
rate=rate,
company=self.company,
customer=self.supplier,
item_code=self.item,
item_name=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
parent_cost_center=self.cost_center,
update_stock=0,
currency="INR",
is_pos=0,
is_return=0,
return_against=None,
income_account=self.income_account,
expense_account=self.expense_account,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return pord
def clear_old_entries(self): def clear_old_entries(self):
doctype_list = [ doctype_list = [
"GL Entry", "GL Entry",
@@ -163,13 +240,11 @@ class TestPaymentReconciliation(FrappeTestCase):
for doctype in doctype_list: for doctype in doctype_list:
qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run() qb.from_(qb.DocType(doctype)).delete().where(qb.DocType(doctype).company == self.company).run()
def create_payment_reconciliation(self): def create_payment_reconciliation(self, party_is_customer=True):
pr = frappe.new_doc("Payment Reconciliation") pr = frappe.new_doc("Payment Reconciliation")
pr.company = self.company pr.company = self.company
pr.party_type = ( pr.party_type = "Customer" if party_is_customer else "Supplier"
self.party_type if hasattr(self, "party_type") and self.party_type else "Customer" pr.party = self.customer if party_is_customer else self.supplier
)
pr.party = self.customer
pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company) pr.receivable_payable_account = get_party_account(pr.party_type, pr.party, pr.company)
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate() pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
return pr return pr
@@ -906,9 +981,13 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(pr.allocation[0].difference_amount, 0) self.assertEqual(pr.allocation[0].difference_amount, 0)
def test_reconciliation_purchase_invoice_against_return(self): def test_reconciliation_purchase_invoice_against_return(self):
pi = make_purchase_invoice( self.supplier = "_Test Supplier USD"
supplier="_Test Supplier USD", currency="USD", conversion_rate=50 pi = self.create_purchase_invoice(qty=5, rate=50, do_not_submit=True)
).submit() pi.supplier = self.supplier
pi.currency = "USD"
pi.conversion_rate = 50
pi.credit_to = self.creditors_usd
pi.save().submit()
pi_return = frappe.get_doc(pi.as_dict()) pi_return = frappe.get_doc(pi.as_dict())
pi_return.name = None pi_return.name = None
@@ -918,11 +997,12 @@ class TestPaymentReconciliation(FrappeTestCase):
pi_return.items[0].qty = -pi_return.items[0].qty pi_return.items[0].qty = -pi_return.items[0].qty
pi_return.submit() pi_return.submit()
self.company = "_Test Company" pr = frappe.get_doc("Payment Reconciliation")
self.party_type = "Supplier" pr.company = self.company
self.customer = "_Test Supplier USD" pr.party_type = "Supplier"
pr.party = self.supplier
pr = self.create_payment_reconciliation() pr.receivable_payable_account = self.creditors_usd
pr.from_invoice_date = pr.to_invoice_date = pr.from_payment_date = pr.to_payment_date = nowdate()
pr.get_unreconciled_entries() pr.get_unreconciled_entries()
invoices = [] invoices = []
@@ -931,6 +1011,7 @@ class TestPaymentReconciliation(FrappeTestCase):
if invoice.invoice_number == pi.name: if invoice.invoice_number == pi.name:
invoices.append(invoice.as_dict()) invoices.append(invoice.as_dict())
break break
for payment in pr.payments: for payment in pr.payments:
if payment.reference_name == pi_return.name: if payment.reference_name == pi_return.name:
payments.append(payment.as_dict()) payments.append(payment.as_dict())
@@ -941,6 +1022,121 @@ class TestPaymentReconciliation(FrappeTestCase):
# Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit. # Should not raise frappe.exceptions.ValidationError: Total Debit must be equal to Total Credit.
pr.reconcile() pr.reconcile()
def test_reconciliation_from_purchase_order_to_multiple_invoices(self):
"""
Reconciling advance payment from PO/SO to multiple invoices should not cause overallocation
"""
self.supplier = "_Test Supplier"
pi1 = self.create_purchase_invoice(qty=10, rate=100)
pi2 = self.create_purchase_invoice(qty=10, rate=100)
po = self.create_purchase_order(qty=20, rate=100)
pay = get_payment_entry(po.doctype, po.name)
# Overpay Puchase Order
pay.paid_amount = 3000
pay.save().submit()
# assert total allocated and unallocated before reconciliation
self.assertEqual(
(
pay.references[0].reference_doctype,
pay.references[0].reference_name,
pay.references[0].allocated_amount,
),
(po.doctype, po.name, 2000),
)
self.assertEqual(pay.total_allocated_amount, 2000)
self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0)
pr = self.create_payment_reconciliation(party_is_customer=False)
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 2)
self.assertEqual(len(pr.payments), 2)
for x in pr.payments:
self.assertEqual((x.reference_type, x.reference_name), (pay.doctype, pay.name))
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# partial allocation on pi1 and full allocate on pi2
pr.allocation[0].allocated_amount = 100
pr.reconcile()
# assert references and total allocated and unallocated amount
pay.reload()
self.assertEqual(len(pay.references), 3)
self.assertEqual(
(
pay.references[0].reference_doctype,
pay.references[0].reference_name,
pay.references[0].allocated_amount,
),
(po.doctype, po.name, 900),
)
self.assertEqual(
(
pay.references[1].reference_doctype,
pay.references[1].reference_name,
pay.references[1].allocated_amount,
),
(pi1.doctype, pi1.name, 100),
)
self.assertEqual(
(
pay.references[2].reference_doctype,
pay.references[2].reference_name,
pay.references[2].allocated_amount,
),
(pi2.doctype, pi2.name, 1000),
)
self.assertEqual(pay.total_allocated_amount, 2000)
self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0)
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
# assert references and total allocated and unallocated amount
pay.reload()
self.assertEqual(len(pay.references), 3)
# PO references should be removed now
self.assertEqual(
(
pay.references[0].reference_doctype,
pay.references[0].reference_name,
pay.references[0].allocated_amount,
),
(pi1.doctype, pi1.name, 100),
)
self.assertEqual(
(
pay.references[1].reference_doctype,
pay.references[1].reference_name,
pay.references[1].allocated_amount,
),
(pi2.doctype, pi2.name, 1000),
)
self.assertEqual(
(
pay.references[2].reference_doctype,
pay.references[2].reference_name,
pay.references[2].allocated_amount,
),
(pi1.doctype, pi1.name, 900),
)
self.assertEqual(pay.total_allocated_amount, 2000)
self.assertEqual(pay.unallocated_amount, 1000)
self.assertEqual(pay.difference_amount, 0)
def make_customer(customer_name, currency=None): def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name): if not frappe.db.exists("Customer", customer_name):

View File

@@ -19,6 +19,7 @@
"is_advance", "is_advance",
"section_break_5", "section_break_5",
"difference_amount", "difference_amount",
"gain_loss_posting_date",
"column_break_7", "column_break_7",
"difference_account", "difference_account",
"exchange_rate", "exchange_rate",
@@ -151,11 +152,16 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Cost Center", "label": "Cost Center",
"options": "Cost Center" "options": "Cost Center"
},
{
"fieldname": "gain_loss_posting_date",
"fieldtype": "Date",
"label": "Difference Posting Date"
} }
], ],
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-09-03 07:52:33.684217", "modified": "2023-10-23 10:44:56.066303",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Payment Reconciliation Allocation", "name": "Payment Reconciliation Allocation",

View File

@@ -33,7 +33,7 @@ from erpnext.accounts.general_ledger import (
) )
from erpnext.accounts.party import get_due_date, get_party_account from erpnext.accounts.party import get_due_date, get_party_account
from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.accounts.utils import get_account_currency, get_fiscal_year
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset.asset import is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.accounts_controller import validate_account_head
@@ -284,9 +284,6 @@ class PurchaseInvoice(BuyingController):
# in case of auto inventory accounting, # in case of auto inventory accounting,
# expense account is always "Stock Received But Not Billed" for a stock item # expense account is always "Stock Received But Not Billed" for a stock item
# except opening entry, drop-ship entry and fixed asset items # except opening entry, drop-ship entry and fixed asset items
if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if ( if (
auto_accounting_for_stock auto_accounting_for_stock
and item.item_code in stock_items and item.item_code in stock_items
@@ -353,22 +350,26 @@ class PurchaseInvoice(BuyingController):
frappe.msgprint(msg, title=_("Expense Head Changed")) frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account item.expense_account = stock_not_billed_account
elif item.is_fixed_asset and item.pr_detail:
elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif item.is_fixed_asset:
account_type = (
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(item.asset_category)
else "fixed_asset_account"
)
asset_category_account = get_asset_category_account( asset_category_account = get_asset_category_account(
"fixed_asset_account", item=item.item_code, company=self.company account_type, item=item.item_code, company=self.company
) )
if not asset_category_account: if not asset_category_account:
form_link = get_link_to_form("Asset Category", asset_category) form_link = get_link_to_form("Asset Category", item.asset_category)
throw( throw(
_("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company),
title=_("Missing Account"), title=_("Missing Account"),
) )
item.expense_account = asset_category_account item.expense_account = asset_category_account
elif item.is_fixed_asset and item.pr_detail:
if not asset_received_but_not_billed:
asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
item.expense_account = asset_received_but_not_billed
elif not item.expense_account and for_validate: elif not item.expense_account and for_validate:
throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name))
@@ -591,12 +592,12 @@ class PurchaseInvoice(BuyingController):
def get_gl_entries(self, warehouse_account=None): def get_gl_entries(self, warehouse_account=None):
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company) self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
self.asset_received_but_not_billed = self.get_company_default("asset_received_but_not_billed")
if self.auto_accounting_for_stock: if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed") self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else: else:
self.stock_received_but_not_billed = None self.stock_received_but_not_billed = None
self.expenses_included_in_valuation = None
self.negative_expense_to_be_booked = 0.0 self.negative_expense_to_be_booked = 0.0
gl_entries = [] gl_entries = []
@@ -605,9 +606,6 @@ class PurchaseInvoice(BuyingController):
self.make_item_gl_entries(gl_entries) self.make_item_gl_entries(gl_entries)
self.make_precision_loss_gl_entry(gl_entries) self.make_precision_loss_gl_entry(gl_entries)
if self.check_asset_cwip_enabled():
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries) self.make_internal_transfer_gl_entries(gl_entries)
@@ -711,7 +709,11 @@ class PurchaseInvoice(BuyingController):
if item.item_code: if item.item_code:
asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category")
if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: if (
self.update_stock
and self.auto_accounting_for_stock
and (item.item_code in stock_items or item.is_fixed_asset)
):
# warehouse account # warehouse account
warehouse_debit_amount = self.make_stock_adjustment_entry( warehouse_debit_amount = self.make_stock_adjustment_entry(
gl_entries, item, voucher_wise_stock_value, account_currency gl_entries, item, voucher_wise_stock_value, account_currency
@@ -826,9 +828,7 @@ class PurchaseInvoice(BuyingController):
) )
) )
elif not item.is_fixed_asset or ( else:
item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)
):
expense_account = ( expense_account = (
item.expense_account item.expense_account
if (not item.enable_deferred_expense or self.is_return) if (not item.enable_deferred_expense or self.is_return)
@@ -921,40 +921,6 @@ class PurchaseInvoice(BuyingController):
) )
) )
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
expenses_included_in_asset_valuation = self.get_company_default(
"expenses_included_in_asset_valuation"
)
# Amount added through landed-cost-voucher
gl_entries.append(
self.get_gl_dict(
{
"account": expenses_included_in_asset_valuation,
"against": expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": expense_account,
"against": expenses_included_in_asset_valuation,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
# update gross amount of asset bought through this document # update gross amount of asset bought through this document
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
@@ -979,11 +945,16 @@ class PurchaseInvoice(BuyingController):
(item.purchase_receipt, valuation_tax_accounts), (item.purchase_receipt, valuation_tax_accounts),
) )
stock_rbnb = (
self.asset_received_but_not_billed
if item.is_fixed_asset
else self.stock_received_but_not_billed
)
if not negative_expense_booked_in_pr: if not negative_expense_booked_in_pr:
gl_entries.append( gl_entries.append(
self.get_gl_dict( self.get_gl_dict(
{ {
"account": self.stock_received_but_not_billed, "account": stock_rbnb,
"against": self.supplier, "against": self.supplier,
"debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")),
"remarks": self.remarks or _("Accounting Entry for Stock"), "remarks": self.remarks or _("Accounting Entry for Stock"),
@@ -998,156 +969,12 @@ class PurchaseInvoice(BuyingController):
item.item_tax_amount, item.precision("item_tax_amount") item.item_tax_amount, item.precision("item_tax_amount")
) )
def get_asset_gl_entry(self, gl_entries): assets = frappe.db.get_all(
arbnb_account = None "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
eiiav_account = None )
asset_eiiav_currency = None for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
for item in self.get("items"): frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
if item.is_fixed_asset:
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type")
if not item.expense_account or item_exp_acc_type not in [
"Asset Received But Not Billed",
"Fixed Asset",
]:
if not arbnb_account:
arbnb_account = self.get_company_default("asset_received_but_not_billed")
item.expense_account = arbnb_account
if not self.update_stock:
arbnb_currency = get_account_currency(item.expense_account)
gl_entries.append(
self.get_gl_dict(
{
"account": item.expense_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount,
"debit_in_account_currency": (
base_asset_amount if arbnb_currency == self.company_currency else asset_amount
),
"cost_center": item.cost_center,
"project": item.project or self.project,
},
item=item,
)
)
if item.item_tax_amount:
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"cost_center": item.cost_center,
"project": item.project or self.project,
"credit": item.item_tax_amount,
"credit_in_account_currency": (
item.item_tax_amount
if asset_eiiav_currency == self.company_currency
else item.item_tax_amount / self.conversion_rate
),
},
item=item,
)
)
else:
cwip_account = get_asset_account(
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
)
cwip_account_currency = get_account_currency(cwip_account)
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"debit": base_asset_amount,
"debit_in_account_currency": (
base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
),
"cost_center": self.cost_center,
"project": item.project or self.project,
},
item=item,
)
)
if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
if not eiiav_account or not asset_eiiav_currency:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
asset_eiiav_currency = get_account_currency(eiiav_account)
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": self.supplier,
"remarks": self.get("remarks") or _("Accounting Entry for Asset"),
"cost_center": item.cost_center,
"credit": item.item_tax_amount,
"project": item.project or self.project,
"credit_in_account_currency": (
item.item_tax_amount
if asset_eiiav_currency == self.company_currency
else item.item_tax_amount / self.conversion_rate
),
},
item=item,
)
)
# Assets are bought through this document then it will be linked to this document
if flt(item.landed_cost_voucher_amount):
if not eiiav_account:
eiiav_account = self.get_company_default("expenses_included_in_asset_valuation")
gl_entries.append(
self.get_gl_dict(
{
"account": eiiav_account,
"against": cwip_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"credit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": cwip_account,
"against": eiiav_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": flt(item.landed_cost_voucher_amount),
"project": item.project or self.project,
},
item=item,
)
)
# update gross amount of assets bought through this document
assets = frappe.db.get_all(
"Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code}
)
for asset in assets:
frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate))
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate))
return gl_entries
def make_stock_adjustment_entry( def make_stock_adjustment_entry(
self, gl_entries, item, voucher_wise_stock_value, account_currency self, gl_entries, item, voucher_wise_stock_value, account_currency

View File

@@ -1858,6 +1858,30 @@ class TestPurchaseInvoice(FrappeTestCase, StockTestMixin):
pi.load_from_db() pi.load_from_db()
self.assertFalse(pi.repost_required) self.assertFalse(pi.repost_required)
def test_default_cost_center_for_purchase(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
for c_center in ["_Test Cost Center Selling", "_Test Cost Center Buying"]:
create_cost_center(cost_center_name=c_center)
item = create_item(
"_Test Cost Center Item For Purchase",
is_stock_item=1,
buying_cost_center="_Test Cost Center Buying - _TC",
selling_cost_center="_Test Cost Center Selling - _TC",
)
pi = make_purchase_invoice(
item=item.name, qty=1, rate=1000, update_stock=True, do_not_submit=True, cost_center=""
)
pi.items[0].cost_center = ""
pi.set_missing_values()
pi.calculate_taxes_and_totals()
pi.save()
self.assertEqual(pi.items[0].cost_center, "_Test Cost Center Buying - _TC")
def check_gl_entries( def check_gl_entries(
doc, doc,

View File

@@ -2497,12 +2497,6 @@ class TestSalesInvoice(FrappeTestCase):
"stock_received_but_not_billed", "stock_received_but_not_billed",
"Stock Received But Not Billed - _TC1", "Stock Received But Not Billed - _TC1",
) )
frappe.db.set_value(
"Company",
"_Test Company 1",
"expenses_included_in_valuation",
"Expenses Included In Valuation - _TC1",
)
# begin test # begin test
si = create_sales_invoice( si = create_sales_invoice(
@@ -2540,7 +2534,7 @@ class TestSalesInvoice(FrappeTestCase):
# tear down # tear down
frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory frappe.local.enable_perpetual_inventory["_Test Company 1"] = old_perpetual_inventory
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock) frappe.db.set_single_value("Stock Settings", "allow_negative_stock", old_negative_stock)
def test_sle_for_target_warehouse(self): def test_sle_for_target_warehouse(self):
se = make_stock_entry( se = make_stock_entry(
@@ -2552,6 +2546,7 @@ class TestSalesInvoice(FrappeTestCase):
) )
si = frappe.copy_doc(test_records[0]) si = frappe.copy_doc(test_records[0])
si.customer = "_Test Internal Customer 3"
si.update_stock = 1 si.update_stock = 1
si.set_warehouse = "Finished Goods - _TC" si.set_warehouse = "Finished Goods - _TC"
si.set_target_warehouse = "Stores - _TC" si.set_target_warehouse = "Stores - _TC"
@@ -3579,6 +3574,20 @@ def create_internal_parties():
allowed_to_interact_with="_Test Company with perpetual inventory", allowed_to_interact_with="_Test Company with perpetual inventory",
) )
create_internal_customer(
customer_name="_Test Internal Customer 3",
represents_company="_Test Company",
allowed_to_interact_with="_Test Company",
)
account = create_account(
account_name="Unrealized Profit",
parent_account="Current Liabilities - _TC",
company="_Test Company",
)
frappe.db.set_value("Company", "_Test Company", "unrealized_profit_loss_account", account)
create_internal_supplier( create_internal_supplier(
supplier_name="_Test Internal Supplier", supplier_name="_Test Internal Supplier",
represents_company="Wind Power LLC", represents_company="Wind Power LLC",

View File

@@ -18,6 +18,14 @@ frappe.ui.form.on('Subscription', {
} }
}; };
}); });
frm.set_query('sales_tax_template', function () {
return {
filters: {
company: frm.doc.company
}
};
});
}, },
refresh: function(frm) { refresh: function(frm) {

View File

@@ -41,7 +41,7 @@ def make_gl_entries(
from_repost=from_repost, from_repost=from_repost,
) )
save_entries(gl_map, adv_adj, update_outstanding, from_repost) save_entries(gl_map, adv_adj, update_outstanding, from_repost)
# Post GL Map proccess there may no be any GL Entries # Post GL Map process there may no be any GL Entries
elif gl_map: elif gl_map:
frappe.throw( frappe.throw(
_( _(

View File

@@ -67,7 +67,7 @@ def setup_mappers(mappers):
mapping["finance_costs"] = [] mapping["finance_costs"] = []
mapping["finance_costs_adjustments"] = [] mapping["finance_costs_adjustments"] = []
doc = frappe.get_doc("Cash Flow Mapper", mapping["name"]) doc = frappe.get_doc("Cash Flow Mapper", mapping["name"])
mapping_names = [item.name for item in doc.accounts] mapping_names = [item.mapping for item in doc.accounts]
if not mapping_names: if not mapping_names:
continue continue

View File

@@ -624,7 +624,7 @@ def update_reference_in_payment_entry(
"outstanding_amount": d.outstanding_amount, "outstanding_amount": d.outstanding_amount,
"allocated_amount": d.allocated_amount, "allocated_amount": d.allocated_amount,
"exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(), "exchange_rate": d.exchange_rate if d.exchange_gain_loss else payment_entry.get_exchange_rate(),
"exchange_gain_loss": d.exchange_gain_loss, # only populated from invoice in case of advance allocation "exchange_gain_loss": d.exchange_gain_loss,
} }
if d.voucher_detail_no: if d.voucher_detail_no:
@@ -636,28 +636,29 @@ def update_reference_in_payment_entry(
existing_row.reference_doctype, existing_row.reference_name existing_row.reference_doctype, existing_row.reference_name
).set_total_advance_paid() ).set_total_advance_paid()
original_row = existing_row.as_dict().copy() if d.allocated_amount <= existing_row.allocated_amount:
existing_row.update(reference_details) existing_row.allocated_amount -= d.allocated_amount
if d.allocated_amount < original_row.allocated_amount:
new_row = payment_entry.append("references") new_row = payment_entry.append("references")
new_row.docstatus = 1 new_row.docstatus = 1
for field in list(reference_details): for field in list(reference_details):
new_row.set(field, original_row[field]) new_row.set(field, reference_details[field])
new_row.allocated_amount = original_row.allocated_amount - d.allocated_amount
else: else:
new_row = payment_entry.append("references") new_row = payment_entry.append("references")
new_row.docstatus = 1 new_row.docstatus = 1
new_row.update(reference_details) new_row.update(reference_details)
payment_entry.flags.ignore_validate_update_after_submit = True payment_entry.flags.ignore_validate_update_after_submit = True
payment_entry.clear_unallocated_reference_document_rows()
payment_entry.setup_party_account_field() payment_entry.setup_party_account_field()
payment_entry.set_missing_values() payment_entry.set_missing_values()
if not skip_ref_details_update_for_pe: if not skip_ref_details_update_for_pe:
payment_entry.set_missing_ref_details() payment_entry.set_missing_ref_details()
payment_entry.set_amounts() payment_entry.set_amounts()
payment_entry.make_exchange_gain_loss_journal() payment_entry.make_exchange_gain_loss_journal(
frappe._dict({"difference_posting_date": d.difference_posting_date})
)
if not do_not_save: if not do_not_save:
payment_entry.save(ignore_permissions=True) payment_entry.save(ignore_permissions=True)

View File

@@ -186,6 +186,7 @@ class TestAsset(AssetSetup):
def test_is_fixed_asset_set(self): def test_is_fixed_asset_set(self):
asset = create_asset(is_existing_asset=1) asset = create_asset(is_existing_asset=1)
doc = frappe.new_doc("Purchase Invoice") doc = frappe.new_doc("Purchase Invoice")
doc.company = "_Test Company"
doc.supplier = "_Test Supplier" doc.supplier = "_Test Supplier"
doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name}) doc.append("items", {"item_code": "Macbook Pro", "qty": 1, "asset": asset.name})
@@ -487,7 +488,7 @@ class TestAsset(AssetSetup):
self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account) self.assertEqual("Asset Received But Not Billed - _TC", doc.items[0].expense_account)
# CWIP: Capital Work In Progress # Capital Work In Progress
def test_cwip_accounting(self): def test_cwip_accounting(self):
pr = make_purchase_receipt( pr = make_purchase_receipt(
item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location" item_code="Macbook Pro", qty=1, rate=5000, do_not_submit=True, location="Test Location"
@@ -520,7 +521,8 @@ class TestAsset(AssetSetup):
pr.submit() pr.submit()
expected_gle = ( expected_gle = (
("Asset Received But Not Billed - _TC", 0.0, 5250.0), ("_Test Account Shipping Charges - _TC", 0.0, 250.0),
("Asset Received But Not Billed - _TC", 0.0, 5000.0),
("CWIP Account - _TC", 5250.0, 0.0), ("CWIP Account - _TC", 5250.0, 0.0),
) )
@@ -539,9 +541,8 @@ class TestAsset(AssetSetup):
expected_gle = ( expected_gle = (
("_Test Account Service Tax - _TC", 250.0, 0.0), ("_Test Account Service Tax - _TC", 250.0, 0.0),
("_Test Account Shipping Charges - _TC", 250.0, 0.0), ("_Test Account Shipping Charges - _TC", 250.0, 0.0),
("Asset Received But Not Billed - _TC", 5250.0, 0.0), ("Asset Received But Not Billed - _TC", 5000.0, 0.0),
("Creditors - _TC", 0.0, 5500.0), ("Creditors - _TC", 0.0, 5500.0),
("Expenses Included In Asset Valuation - _TC", 0.0, 250.0),
) )
pi_gle = frappe.db.sql( pi_gle = frappe.db.sql(

View File

@@ -522,6 +522,8 @@ def make_purchase_receipt(source_name, target_doc=None):
"bom": "bom", "bom": "bom",
"material_request": "material_request", "material_request": "material_request",
"material_request_item": "material_request_item", "material_request_item": "material_request_item",
"sales_order": "sales_order",
"sales_order_item": "sales_order_item",
}, },
"postprocess": update_item, "postprocess": update_item,
"condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty)

View File

@@ -1131,7 +1131,9 @@ class AccountsController(TransactionBase):
self.name, self.name,
arg.get("referenced_row"), arg.get("referenced_row"),
): ):
posting_date = frappe.db.get_value(arg.voucher_type, arg.voucher_no, "posting_date") posting_date = arg.get("difference_posting_date") or frappe.db.get_value(
arg.voucher_type, arg.voucher_no, "posting_date"
)
je = create_gain_loss_journal( je = create_gain_loss_journal(
self.company, self.company,
posting_date, posting_date,
@@ -1214,7 +1216,7 @@ class AccountsController(TransactionBase):
je = create_gain_loss_journal( je = create_gain_loss_journal(
self.company, self.company,
self.posting_date, args.get("difference_posting_date") if args else self.posting_date,
self.party_type, self.party_type,
self.party, self.party,
party_account, party_account,

View File

@@ -62,9 +62,12 @@ class StockController(AccountsController):
) )
) )
is_asset_pr = any(d.get("is_fixed_asset") for d in self.get("items"))
if ( if (
cint(erpnext.is_perpetual_inventory_enabled(self.company)) cint(erpnext.is_perpetual_inventory_enabled(self.company))
or provisional_accounting_for_non_stock_items or provisional_accounting_for_non_stock_items
or is_asset_pr
): ):
warehouse_account = get_warehouse_account_map(self.company) warehouse_account = get_warehouse_account_map(self.company)
@@ -73,11 +76,6 @@ class StockController(AccountsController):
gl_entries = self.get_gl_entries(warehouse_account) gl_entries = self.get_gl_entries(warehouse_account)
make_gl_entries(gl_entries, from_repost=from_repost) make_gl_entries(gl_entries, from_repost=from_repost)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"] and self.docstatus == 1:
gl_entries = []
gl_entries = self.get_asset_gl_entry(gl_entries)
make_gl_entries(gl_entries, from_repost=from_repost)
def validate_serialized_batch(self): def validate_serialized_batch(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -670,13 +668,21 @@ class StockController(AccountsController):
d.stock_uom_rate = d.rate / (d.conversion_factor or 1) d.stock_uom_rate = d.rate / (d.conversion_factor or 1)
def validate_internal_transfer(self): def validate_internal_transfer(self):
if ( if self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt"):
self.doctype in ("Sales Invoice", "Delivery Note", "Purchase Invoice", "Purchase Receipt") if self.is_internal_transfer():
and self.is_internal_transfer() self.validate_in_transit_warehouses()
): self.validate_multi_currency()
self.validate_in_transit_warehouses() self.validate_packed_items()
self.validate_multi_currency() else:
self.validate_packed_items() self.validate_internal_transfer_warehouse()
def validate_internal_transfer_warehouse(self):
for row in self.items:
if row.get("target_warehouse"):
row.target_warehouse = None
if row.get("from_warehouse"):
row.from_warehouse = None
def validate_in_transit_warehouses(self): def validate_in_transit_warehouses(self):
if ( if (

View File

@@ -7,7 +7,7 @@ import frappe
from frappe import qb from frappe import qb
from frappe.query_builder.functions import Sum from frappe.query_builder.functions import Sum
from frappe.tests.utils import FrappeTestCase, change_settings from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import add_days, flt, nowdate from frappe.utils import add_days, flt, getdate, nowdate
from erpnext import get_default_cost_center from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
@@ -614,6 +614,73 @@ class TestAccountsController(FrappeTestCase):
self.assertEqual(exc_je_for_si, []) self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, []) self.assertEqual(exc_je_for_pe, [])
def test_15_gain_loss_on_different_posting_date(self):
# Invoice in Foreign Currency
si = self.create_sales_invoice(
posting_date=add_days(nowdate(), -2), qty=2, conversion_rate=80, rate=1
)
# Payment
pe = (
self.create_payment_entry(posting_date=add_days(nowdate(), -1), amount=2, source_exc_rate=75)
.save()
.submit()
)
# There should be outstanding in both currencies
si.reload()
self.assertEqual(si.outstanding_amount, 2)
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
# Reconcile the remaining amount
pr = frappe.get_doc("Payment Reconciliation")
pr.company = self.company
pr.party_type = "Customer"
pr.party = self.customer
pr.receivable_payable_account = self.debit_usd
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].gain_loss_posting_date = add_days(nowdate(), 1)
pr.reconcile()
# Exchange Gain/Loss Journal should've been created.
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
self.assertNotEqual(exc_je_for_si, [])
self.assertEqual(len(exc_je_for_si), 1)
self.assertEqual(len(exc_je_for_pe), 1)
self.assertEqual(exc_je_for_si[0], exc_je_for_pe[0])
self.assertEqual(
frappe.db.get_value("Journal Entry", exc_je_for_si[0].parent, "posting_date"),
getdate(add_days(nowdate(), 1)),
)
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
# There should be no outstanding
si.reload()
self.assertEqual(si.outstanding_amount, 0)
self.assert_ledger_outstanding(si.doctype, si.name, 0.0, 0.0)
# Cancel Payment
pe.reload()
pe.cancel()
si.reload()
self.assertEqual(si.outstanding_amount, 2)
self.assert_ledger_outstanding(si.doctype, si.name, 160.0, 2.0)
# Exchange Gain/Loss Journal should've been cancelled
exc_je_for_si = self.get_journals_for(si.doctype, si.name)
exc_je_for_pe = self.get_journals_for(pe.doctype, pe.name)
self.assertEqual(exc_je_for_si, [])
self.assertEqual(exc_je_for_pe, [])
def test_20_journal_against_sales_invoice(self): def test_20_journal_against_sales_invoice(self):
# Invoice in Foreign Currency # Invoice in Foreign Currency
si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1) si = self.create_sales_invoice(qty=1, conversion_rate=80, rate=1)

View File

@@ -411,11 +411,6 @@ def close_unsecured_term_loan(loan):
frappe.throw(_("Cannot close this loan until full repayment")) frappe.throw(_("Cannot close this loan until full repayment"))
def close_loan(loan, total_amount_paid):
frappe.db.set_value("Loan", loan, "total_amount_paid", total_amount_paid)
frappe.db.set_value("Loan", loan, "status", "Closed")
@frappe.whitelist() @frappe.whitelist()
def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0): def make_loan_disbursement(loan, company, applicant_type, applicant, pending_amount=0, as_dict=0):
disbursement_entry = frappe.new_doc("Loan Disbursement") disbursement_entry = frappe.new_doc("Loan Disbursement")

View File

@@ -9,6 +9,9 @@ from frappe.utils import cint, flt, getdate
import erpnext import erpnext
from erpnext.accounts.general_ledger import make_gl_entries from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import (
get_pending_principal_amount,
)
class LoanWriteOff(AccountsController): class LoanWriteOff(AccountsController):
@@ -39,11 +42,13 @@ class LoanWriteOff(AccountsController):
def on_submit(self): def on_submit(self):
self.update_outstanding_amount() self.update_outstanding_amount()
self.make_gl_entries() self.make_gl_entries()
self.close_employee_loan()
def on_cancel(self): def on_cancel(self):
self.update_outstanding_amount(cancel=1) self.update_outstanding_amount(cancel=1)
self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"] self.ignore_linked_doctypes = ["GL Entry", "Payment Ledger Entry"]
self.make_gl_entries(cancel=1) self.make_gl_entries(cancel=1)
self.close_employee_loan(cancel=1)
def update_outstanding_amount(self, cancel=0): def update_outstanding_amount(self, cancel=0):
written_off_amount = frappe.db.get_value("Loan", self.loan, "written_off_amount") written_off_amount = frappe.db.get_value("Loan", self.loan, "written_off_amount")
@@ -94,3 +99,39 @@ class LoanWriteOff(AccountsController):
) )
make_gl_entries(gl_entries, cancel=cancel, merge_entries=False) make_gl_entries(gl_entries, cancel=cancel, merge_entries=False)
def close_employee_loan(self, cancel=0):
if not frappe.db.has_column("Loan", "repay_from_salary"):
return
loan = frappe.get_value(
"Loan",
self.loan,
[
"total_payment",
"total_principal_paid",
"loan_amount",
"total_interest_payable",
"written_off_amount",
"disbursed_amount",
"status",
"is_secured_loan",
"repay_from_salary",
"name",
],
as_dict=1,
)
if loan.is_secured_loan or not loan.repay_from_salary:
return
if not cancel:
pending_principal_amount = get_pending_principal_amount(loan)
precision = cint(frappe.db.get_default("currency_precision")) or 2
if flt(pending_principal_amount, precision) <= 0:
frappe.db.set_value("Loan", loan.name, "status", "Closed")
frappe.msgprint(_("Loan {0} closed").format(loan.name))
else:
frappe.db.set_value("Loan", loan.loan, "status", "Disbursed")

View File

@@ -1172,12 +1172,12 @@ def get_children(parent=None, is_root=False, **filters):
def add_additional_cost(stock_entry, work_order): def add_additional_cost(stock_entry, work_order):
# Add non stock items cost in the additional cost # Add non stock items cost in the additional cost
stock_entry.additional_costs = [] stock_entry.additional_costs = []
expenses_included_in_valuation = frappe.get_cached_value( default_expense_account = frappe.get_cached_value(
"Company", work_order.company, "expenses_included_in_valuation" "Company", work_order.company, "default_expense_account"
) )
add_non_stock_items_cost(stock_entry, work_order, expenses_included_in_valuation) add_non_stock_items_cost(stock_entry, work_order, default_expense_account)
add_operations_cost(stock_entry, work_order, expenses_included_in_valuation) add_operations_cost(stock_entry, work_order, default_expense_account)
def add_non_stock_items_cost(stock_entry, work_order, expense_account): def add_non_stock_items_cost(stock_entry, work_order, expense_account):

View File

@@ -1,42 +0,0 @@
import frappe
def execute():
frappe.reload_doctype("Landed Cost Taxes and Charges")
company_account_map = frappe._dict(
frappe.db.sql(
"""
SELECT name, expenses_included_in_valuation from `tabCompany`
"""
)
)
for company, account in company_account_map.items():
frappe.db.sql(
"""
UPDATE
`tabLanded Cost Taxes and Charges` t, `tabLanded Cost Voucher` l
SET
t.expense_account = %s
WHERE
l.docstatus = 1
AND l.company = %s
AND t.parent = l.name
""",
(account, company),
)
frappe.db.sql(
"""
UPDATE
`tabLanded Cost Taxes and Charges` t, `tabStock Entry` s
SET
t.expense_account = %s
WHERE
s.docstatus = 1
AND s.company = %s
AND t.parent = s.name
""",
(account, company),
)

View File

@@ -17,7 +17,7 @@ erpnext.accounts.unreconcile_payments = {
}, },
callback: function(r) { callback: function(r) {
if (r.message) { if (r.message) {
frm.add_custom_button(__("Un-Reconcile"), function() { frm.add_custom_button(__("UnReconcile"), function() {
erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm); erpnext.accounts.unreconcile_payments.build_unreconcile_dialog(frm);
}, __('Actions')); }, __('Actions'));
} }
@@ -87,11 +87,11 @@ erpnext.accounts.unreconcile_payments = {
unreconcile_dialog_fields[0].get_data = function(){ return r.message}; unreconcile_dialog_fields[0].get_data = function(){ return r.message};
let d = new frappe.ui.Dialog({ let d = new frappe.ui.Dialog({
title: 'Un-Reconcile Allocations', title: 'UnReconcile Allocations',
fields: unreconcile_dialog_fields, fields: unreconcile_dialog_fields,
size: 'large', size: 'large',
cannot_add_rows: true, cannot_add_rows: true,
primary_action_label: 'Un-Reconcile', primary_action_label: 'UnReconcile',
primary_action(values) { primary_action(values) {
let selected_allocations = values.allocations.filter(x=>x.__checked); let selected_allocations = values.allocations.filter(x=>x.__checked);

View File

@@ -1631,7 +1631,7 @@
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-04-20 11:14:01.036202", "modified": "2023-10-18 12:41:54.813462",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@@ -1893,10 +1893,10 @@ class TestSalesOrder(FrappeTestCase):
si.submit() si.submit()
pe.load_from_db() pe.load_from_db()
self.assertEqual(pe.references[0].reference_name, si.name) self.assertEqual(pe.references[0].reference_name, so.name)
self.assertEqual(pe.references[0].allocated_amount, 200) self.assertEqual(pe.references[0].allocated_amount, 300)
self.assertEqual(pe.references[1].reference_name, so.name) self.assertEqual(pe.references[1].reference_name, si.name)
self.assertEqual(pe.references[1].allocated_amount, 300) self.assertEqual(pe.references[1].allocated_amount, 200)
def test_delivered_item_material_request(self): def test_delivered_item_material_request(self):
"SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO." "SO -> MR (Manufacture) -> WO. Test if WO Qty is updated in SO."

View File

@@ -223,7 +223,6 @@ erpnext.company.setup_queries = function(frm) {
["cost_center", {}], ["cost_center", {}],
["round_off_cost_center", {}], ["round_off_cost_center", {}],
["depreciation_cost_center", {}], ["depreciation_cost_center", {}],
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}], ["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}], ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}], ["unrealized_profit_loss_account", {"root_type": ["in", ["Liability", "Asset"]]}],
@@ -236,8 +235,6 @@ erpnext.company.setup_queries = function(frm) {
$.each([ $.each([
["stock_adjustment_account", ["stock_adjustment_account",
{"root_type": "Expense", "account_type": "Stock Adjustment"}], {"root_type": "Expense", "account_type": "Stock Adjustment"}],
["expenses_included_in_valuation",
{"root_type": "Expense", "account_type": "Expenses Included in Valuation"}],
["stock_received_but_not_billed", ["stock_received_but_not_billed",
{"root_type": "Liability", "account_type": "Stock Received But Not Billed"}], {"root_type": "Liability", "account_type": "Stock Received But Not Billed"}],
["service_received_but_not_billed", ["service_received_but_not_billed",

View File

@@ -79,12 +79,10 @@
"column_break_32", "column_break_32",
"stock_received_but_not_billed", "stock_received_but_not_billed",
"default_provisional_account", "default_provisional_account",
"expenses_included_in_valuation",
"fixed_asset_defaults", "fixed_asset_defaults",
"accumulated_depreciation_account", "accumulated_depreciation_account",
"depreciation_expense_account", "depreciation_expense_account",
"series_for_depreciation_entry", "series_for_depreciation_entry",
"expenses_included_in_asset_valuation",
"column_break_40", "column_break_40",
"disposal_account", "disposal_account",
"depreciation_cost_center", "depreciation_cost_center",
@@ -466,14 +464,6 @@
"no_copy": 1, "no_copy": 1,
"options": "Account" "options": "Account"
}, },
{
"fieldname": "expenses_included_in_valuation",
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Expenses Included In Valuation",
"no_copy": 1,
"options": "Account"
},
{ {
"fieldname": "accumulated_depreciation_account", "fieldname": "accumulated_depreciation_account",
"fieldtype": "Link", "fieldtype": "Link",
@@ -493,12 +483,6 @@
"fieldtype": "Data", "fieldtype": "Data",
"label": "Series for Asset Depreciation Entry (Journal Entry)" "label": "Series for Asset Depreciation Entry (Journal Entry)"
}, },
{
"fieldname": "expenses_included_in_asset_valuation",
"fieldtype": "Link",
"label": "Expenses Included In Asset Valuation",
"options": "Account"
},
{ {
"fieldname": "column_break_40", "fieldname": "column_break_40",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -728,7 +712,7 @@
"image_field": "company_logo", "image_field": "company_logo",
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2023-07-07 05:41:41.537256", "modified": "2023-10-23 10:19:24.322898",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Setup", "module": "Setup",
"name": "Company", "name": "Company",

View File

@@ -92,7 +92,6 @@ class Company(NestedSet):
["Default Income Account", "default_income_account"], ["Default Income Account", "default_income_account"],
["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Received But Not Billed Account", "stock_received_but_not_billed"],
["Stock Adjustment Account", "stock_adjustment_account"], ["Stock Adjustment Account", "stock_adjustment_account"],
["Expense Included In Valuation Account", "expenses_included_in_valuation"],
] ]
for account in accounts: for account in accounts:
@@ -384,7 +383,6 @@ class Company(NestedSet):
"depreciation_expense_account": "Depreciation", "depreciation_expense_account": "Depreciation",
"capital_work_in_progress_account": "Capital Work in Progress", "capital_work_in_progress_account": "Capital Work in Progress",
"asset_received_but_not_billed": "Asset Received But Not Billed", "asset_received_but_not_billed": "Asset Received But Not Billed",
"expenses_included_in_asset_valuation": "Expenses Included In Asset Valuation",
"default_expense_account": "Cost of Goods Sold", "default_expense_account": "Cost of Goods Sold",
} }
@@ -394,7 +392,6 @@ class Company(NestedSet):
"stock_received_but_not_billed": "Stock Received But Not Billed", "stock_received_but_not_billed": "Stock Received But Not Billed",
"default_inventory_account": "Stock", "default_inventory_account": "Stock",
"stock_adjustment_account": "Stock Adjustment", "stock_adjustment_account": "Stock Adjustment",
"expenses_included_in_valuation": "Expenses Included In Valuation",
} }
) )

View File

@@ -1315,6 +1315,21 @@ class TestDeliveryNote(FrappeTestCase):
frappe.db.rollback() frappe.db.rollback()
frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0) frappe.db.set_single_value("Selling Settings", "dont_reserve_sales_order_qty_on_sales_return", 0)
def non_internal_transfer_delivery_note(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
dn = create_delivery_note(do_not_submit=True)
warehouse = create_warehouse("Internal Transfer Warehouse", dn.company)
dn.items[0].db_set("target_warehouse", "warehouse")
dn.reload()
self.assertEqual(dn.items[0].target_warehouse, warehouse.name)
dn.save()
dn.reload()
self.assertFalse(dn.items[0].target_warehouse)
def create_delivery_note(**args): def create_delivery_note(**args):
dn = frappe.new_doc("Delivery Note") dn = frappe.new_doc("Delivery Note")

View File

@@ -62,7 +62,7 @@ frappe.ui.form.on('Delivery Trip', {
company: frm.doc.company, company: frm.doc.company,
} }
}) })
}, __("Get customers from")); }, __("Get stops from"));
} }
}, },

View File

@@ -893,6 +893,8 @@ def create_item(
opening_stock=0, opening_stock=0,
is_fixed_asset=0, is_fixed_asset=0,
asset_category=None, asset_category=None,
buying_cost_center=None,
selling_cost_center=None,
company="_Test Company", company="_Test Company",
): ):
if not frappe.db.exists("Item", item_code): if not frappe.db.exists("Item", item_code):
@@ -910,7 +912,15 @@ def create_item(
item.is_purchase_item = is_purchase_item item.is_purchase_item = is_purchase_item
item.is_customer_provided_item = is_customer_provided_item item.is_customer_provided_item = is_customer_provided_item
item.customer = customer or "" item.customer = customer or ""
item.append("item_defaults", {"default_warehouse": warehouse, "company": company}) item.append(
"item_defaults",
{
"default_warehouse": warehouse,
"company": company,
"selling_cost_center": selling_cost_center,
"buying_cost_center": buying_cost_center,
},
)
item.save() item.save()
else: else:
item = frappe.get_doc("Item", item_code) item = frappe.get_doc("Item", item_code)

View File

@@ -13,7 +13,6 @@ from pypika import functions as fn
import erpnext import erpnext
from erpnext.accounts.utils import get_account_currency from erpnext.accounts.utils import get_account_currency
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
from erpnext.buying.utils import check_on_hold_or_closed_status from erpnext.buying.utils import check_on_hold_or_closed_status
from erpnext.controllers.buying_controller import BuyingController from erpnext.controllers.buying_controller import BuyingController
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_transaction
@@ -146,8 +145,8 @@ class PurchaseReceipt(BuyingController):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
# check cwip accounts before making auto assets # check cwip accounts before making auto assets
# Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account
arbnb_account = self.get_company_default("asset_received_but_not_billed") self.get_company_default("asset_received_but_not_billed")
cwip_account = get_asset_account( get_asset_account(
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
) )
break break
@@ -315,7 +314,6 @@ class PurchaseReceipt(BuyingController):
self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account) self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
self.make_tax_gl_entries(gl_entries) self.make_tax_gl_entries(gl_entries)
self.get_asset_gl_entry(gl_entries)
return process_gl_map(gl_entries) return process_gl_map(gl_entries)
@@ -324,14 +322,6 @@ class PurchaseReceipt(BuyingController):
get_purchase_document_details, get_purchase_document_details,
) )
stock_rbnb = None
if erpnext.is_perpetual_inventory_enabled(self.company):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
warehouse_with_no_account = []
stock_items = self.get_stock_items()
provisional_accounting_for_non_stock_items = cint( provisional_accounting_for_non_stock_items = cint(
frappe.db.get_value( frappe.db.get_value(
"Company", self.company, "enable_provisional_accounting_for_non_stock_items" "Company", self.company, "enable_provisional_accounting_for_non_stock_items"
@@ -340,28 +330,256 @@ class PurchaseReceipt(BuyingController):
exchange_rate_map, net_rate_map = get_purchase_document_details(self) exchange_rate_map, net_rate_map = get_purchase_document_details(self)
def validate_account(account_type):
frappe.throw(_("{0} account not found while submitting purchase receipt").format(account_type))
def make_item_asset_inward_gl_entry(item, stock_value_diff, stock_asset_account_name):
account_currency = get_account_currency(stock_asset_account_name)
if not stock_asset_account_name:
validate_account("Asset or warehouse account")
self.add_gl_entry(
gl_entries=gl_entries,
account=stock_asset_account_name,
cost_center=d.cost_center,
debit=stock_value_diff,
credit=0.0,
remarks=remarks,
against_account=stock_asset_rbnb,
account_currency=account_currency,
item=item,
)
def make_stock_received_but_not_billed_entry(item):
account = (
warehouse_account[item.from_warehouse]["account"] if item.from_warehouse else stock_asset_rbnb
)
account_currency = get_account_currency(account)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
credit_amount = (
flt(item.base_net_amount, item.precision("base_net_amount"))
if account_currency == self.company_currency
else flt(item.net_amount, item.precision("net_amount"))
)
outgoing_amount = item.base_net_amount
if self.is_internal_transfer() and item.valuation_rate:
outgoing_amount = abs(get_stock_value_difference(self.name, item.name, item.from_warehouse))
credit_amount = outgoing_amount
if credit_amount:
if not account:
validate_account("Stock or Asset Received But Not Billed")
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=item.cost_center,
debit=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
credit=0.0,
remarks=remarks,
against_account=stock_asset_account_name,
debit_in_account_currency=-1 * flt(outgoing_amount, item.precision("base_net_amount")),
account_currency=account_currency,
item=item,
)
# check if the exchange rate has changed
if d.get("purchase_invoice"):
if (
exchange_rate_map[item.purchase_invoice]
and self.conversion_rate != exchange_rate_map[item.purchase_invoice]
and item.net_rate == net_rate_map[item.purchase_invoice_item]
):
discrepancy_caused_by_exchange_rate_difference = (item.qty * item.net_rate) * (
exchange_rate_map[item.purchase_invoice] - self.conversion_rate
)
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=item.cost_center,
debit=0.0,
credit=discrepancy_caused_by_exchange_rate_difference,
remarks=remarks,
against_account=self.supplier,
debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=account_currency,
item=item,
)
self.add_gl_entry(
gl_entries=gl_entries,
account=self.get_company_default("exchange_gain_loss_account"),
cost_center=d.cost_center,
debit=discrepancy_caused_by_exchange_rate_difference,
credit=0.0,
remarks=remarks,
against_account=self.supplier,
debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=account_currency,
item=item,
)
return outgoing_amount
def make_landed_cost_gl_entries(item):
# Amount added through landed-cost-voucher
if item.landed_cost_voucher_amount and landed_cost_entries:
if (item.item_code, item.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(item.item_code, item.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
flt(amount["base_amount"])
if (amount["base_amount"] or account_currency != self.company_currency)
else flt(amount["amount"])
)
if not account:
validate_account("Landed Cost Account")
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=item.cost_center,
debit=0.0,
credit=credit_amount,
remarks=remarks,
against_account=stock_asset_account_name,
credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=item.project,
item=item,
)
def make_rate_difference_entry(item):
if item.rate_difference_with_purchase_invoice and stock_asset_rbnb:
account_currency = get_account_currency(stock_asset_rbnb)
self.add_gl_entry(
gl_entries=gl_entries,
account=stock_asset_rbnb,
cost_center=item.cost_center,
debit=0.0,
credit=flt(item.rate_difference_with_purchase_invoice),
remarks=_("Adjustment based on Purchase Invoice rate"),
against_account=stock_asset_account_name,
account_currency=account_currency,
project=item.project,
item=item,
)
def make_sub_contracting_gl_entries(item):
# sub-contracting warehouse
if flt(item.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
self.add_gl_entry(
gl_entries=gl_entries,
account=supplier_warehouse_account,
cost_center=item.cost_center,
debit=0.0,
credit=flt(item.rm_supp_cost),
remarks=remarks,
against_account=stock_asset_account_name,
account_currency=supplier_warehouse_account_currency,
item=item,
)
def make_divisional_loss_gl_entry(item, outgoing_amount):
if item.is_fixed_asset:
return
# divisional loss adjustment
valuation_amount_as_per_doc = (
flt(outgoing_amount, d.precision("base_net_amount"))
+ flt(item.landed_cost_voucher_amount)
+ flt(item.rm_supp_cost)
+ flt(item.item_tax_amount)
+ flt(item.rate_difference_with_purchase_invoice)
)
divisional_loss = flt(
valuation_amount_as_per_doc - flt(stock_value_diff), item.precision("base_net_amount")
)
if divisional_loss:
loss_account = (
self.get_company_default("default_expense_account", ignore_validation=True)
or stock_asset_rbnb
)
cost_center = item.cost_center or frappe.get_cached_value(
"Company", self.company, "cost_center"
)
account_currency = get_account_currency(loss_account)
self.add_gl_entry(
gl_entries=gl_entries,
account=loss_account,
cost_center=cost_center,
debit=divisional_loss,
credit=0.0,
remarks=remarks,
against_account=stock_asset_account_name,
account_currency=account_currency,
project=item.project,
item=item,
)
stock_items = self.get_stock_items()
warehouse_with_no_account = []
for d in self.get("items"): for d in self.get("items"):
if d.item_code in stock_items and flt(d.qty) and (flt(d.valuation_rate) or self.is_return): if (
if warehouse_account.get(d.warehouse): provisional_accounting_for_non_stock_items
stock_value_diff = frappe.db.get_value( and d.item_code not in stock_items
"Stock Ledger Entry", and flt(d.qty)
{ and d.get("provisional_expense_account")
"voucher_type": "Purchase Receipt", and not d.is_fixed_asset
"voucher_no": self.name, ):
"voucher_detail_no": d.name, self.add_provisional_gl_entry(
"warehouse": d.warehouse, d, gl_entries, self.posting_date, d.get("provisional_expense_account")
"is_cancelled": 0, )
}, elif flt(d.qty) and (flt(d.valuation_rate) or self.is_return):
"stock_value_difference", is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
remarks = self.get("remarks") or _("Accounting Entry for {0}").format(
"Asset" if is_asset_pr else "Stock"
)
if not (erpnext.is_perpetual_inventory_enabled(self.company) or is_asset_pr):
return
stock_asset_rbnb = (
self.get_company_default("asset_received_but_not_billed")
if is_asset_pr
else self.get_company_default("stock_received_but_not_billed")
)
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
if d.is_fixed_asset:
account_type = (
"capital_work_in_progress_account"
if is_cwip_accounting_enabled(d.asset_category)
else "fixed_asset_account"
) )
warehouse_account_name = warehouse_account[d.warehouse]["account"] stock_asset_account_name = get_asset_account(
warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"] account_type, asset_category=d.asset_category, company=self.company
)
stock_value_diff = (
flt(d.net_amount)
+ flt(d.item_tax_amount / self.conversion_rate)
+ flt(d.landed_cost_voucher_amount)
)
elif warehouse_account.get(d.warehouse):
stock_value_diff = get_stock_value_difference(self.name, d.name, d.warehouse)
stock_asset_account_name = warehouse_account[d.warehouse]["account"]
supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account") supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get( supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get(
"account_currency" "account_currency"
) )
remarks = self.get("remarks") or _("Accounting Entry for Stock")
# If PR is sub-contracted and fg item rate is zero # If PR is sub-contracted and fg item rate is zero
# in that case if account for source and target warehouse are same, # in that case if account for source and target warehouse are same,
@@ -369,214 +587,25 @@ class PurchaseReceipt(BuyingController):
if ( if (
flt(stock_value_diff) == flt(d.rm_supp_cost) flt(stock_value_diff) == flt(d.rm_supp_cost)
and warehouse_account.get(self.supplier_warehouse) and warehouse_account.get(self.supplier_warehouse)
and warehouse_account_name == supplier_warehouse_account and stock_asset_account_name == supplier_warehouse_account
): ):
continue continue
self.add_gl_entry( if (flt(d.valuation_rate) or self.is_return or d.is_fixed_asset) and flt(d.qty):
gl_entries=gl_entries, make_item_asset_inward_gl_entry(d, stock_value_diff, stock_asset_account_name)
account=warehouse_account_name, outgoing_amount = make_stock_received_but_not_billed_entry(d)
cost_center=d.cost_center, make_landed_cost_gl_entries(d)
debit=stock_value_diff, make_rate_difference_entry(d)
credit=0.0, make_sub_contracting_gl_entries(d)
remarks=remarks, make_divisional_loss_gl_entry(d, outgoing_amount)
against_account=stock_rbnb,
account_currency=warehouse_account_currency,
item=d,
)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
credit_currency = (
get_account_currency(warehouse_account[d.from_warehouse]["account"])
if d.from_warehouse
else get_account_currency(stock_rbnb)
)
credit_amount = (
flt(d.base_net_amount, d.precision("base_net_amount"))
if credit_currency == self.company_currency
else flt(d.net_amount, d.precision("net_amount"))
)
outgoing_amount = d.base_net_amount
if self.is_internal_transfer() and d.valuation_rate:
outgoing_amount = abs(
frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": self.name,
"voucher_detail_no": d.name,
"warehouse": d.from_warehouse,
"is_cancelled": 0,
},
"stock_value_difference",
)
)
credit_amount = outgoing_amount
if credit_amount:
account = warehouse_account[d.from_warehouse]["account"] if d.from_warehouse else stock_rbnb
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=d.cost_center,
debit=-1 * flt(outgoing_amount, d.precision("base_net_amount")),
credit=0.0,
remarks=remarks,
against_account=warehouse_account_name,
debit_in_account_currency=-1 * credit_amount,
account_currency=credit_currency,
item=d,
)
# check if the exchange rate has changed
if d.get("purchase_invoice"):
if (
exchange_rate_map[d.purchase_invoice]
and self.conversion_rate != exchange_rate_map[d.purchase_invoice]
and d.net_rate == net_rate_map[d.purchase_invoice_item]
):
discrepancy_caused_by_exchange_rate_difference = (d.qty * d.net_rate) * (
exchange_rate_map[d.purchase_invoice] - self.conversion_rate
)
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=d.cost_center,
debit=0.0,
credit=discrepancy_caused_by_exchange_rate_difference,
remarks=remarks,
against_account=self.supplier,
debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency,
item=d,
)
self.add_gl_entry(
gl_entries=gl_entries,
account=self.get_company_default("exchange_gain_loss_account"),
cost_center=d.cost_center,
debit=discrepancy_caused_by_exchange_rate_difference,
credit=0.0,
remarks=remarks,
against_account=self.supplier,
debit_in_account_currency=-1 * discrepancy_caused_by_exchange_rate_difference,
account_currency=credit_currency,
item=d,
)
# Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
if (d.item_code, d.name) in landed_cost_entries:
for account, amount in landed_cost_entries[(d.item_code, d.name)].items():
account_currency = get_account_currency(account)
credit_amount = (
flt(amount["base_amount"])
if (amount["base_amount"] or account_currency != self.company_currency)
else flt(amount["amount"])
)
self.add_gl_entry(
gl_entries=gl_entries,
account=account,
cost_center=d.cost_center,
debit=0.0,
credit=credit_amount,
remarks=remarks,
against_account=warehouse_account_name,
credit_in_account_currency=flt(amount["amount"]),
account_currency=account_currency,
project=d.project,
item=d,
)
if d.rate_difference_with_purchase_invoice and stock_rbnb:
account_currency = get_account_currency(stock_rbnb)
self.add_gl_entry(
gl_entries=gl_entries,
account=stock_rbnb,
cost_center=d.cost_center,
debit=0.0,
credit=flt(d.rate_difference_with_purchase_invoice),
remarks=_("Adjustment based on Purchase Invoice rate"),
against_account=warehouse_account_name,
account_currency=account_currency,
project=d.project,
item=d,
)
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
self.add_gl_entry(
gl_entries=gl_entries,
account=supplier_warehouse_account,
cost_center=d.cost_center,
debit=0.0,
credit=flt(d.rm_supp_cost),
remarks=remarks,
against_account=warehouse_account_name,
account_currency=supplier_warehouse_account_currency,
item=d,
)
# divisional loss adjustment
valuation_amount_as_per_doc = (
flt(outgoing_amount, d.precision("base_net_amount"))
+ flt(d.landed_cost_voucher_amount)
+ flt(d.rm_supp_cost)
+ flt(d.item_tax_amount)
+ flt(d.rate_difference_with_purchase_invoice)
)
divisional_loss = flt(
valuation_amount_as_per_doc - flt(stock_value_diff), d.precision("base_net_amount")
)
if divisional_loss:
if self.is_return or flt(d.item_tax_amount):
loss_account = expenses_included_in_valuation
else:
loss_account = (
self.get_company_default("default_expense_account", ignore_validation=True) or stock_rbnb
)
cost_center = d.cost_center or frappe.get_cached_value(
"Company", self.company, "cost_center"
)
self.add_gl_entry(
gl_entries=gl_entries,
account=loss_account,
cost_center=cost_center,
debit=divisional_loss,
credit=0.0,
remarks=remarks,
against_account=warehouse_account_name,
account_currency=credit_currency,
project=d.project,
item=d,
)
elif (
d.warehouse not in warehouse_with_no_account
or d.rejected_warehouse not in warehouse_with_no_account
):
warehouse_with_no_account.append(d.warehouse)
elif ( elif (
d.item_code not in stock_items d.warehouse not in warehouse_with_no_account
and not d.is_fixed_asset or d.rejected_warehouse not in warehouse_with_no_account
and flt(d.qty)
and provisional_accounting_for_non_stock_items
and d.get("provisional_expense_account")
): ):
self.add_provisional_gl_entry( warehouse_with_no_account.append(d.warehouse)
d, gl_entries, self.posting_date, d.get("provisional_expense_account")
) if d.is_fixed_asset:
self.update_assets(d, d.valuation_rate)
if warehouse_with_no_account: if warehouse_with_no_account:
frappe.msgprint( frappe.msgprint(
@@ -589,8 +618,8 @@ class PurchaseReceipt(BuyingController):
self, item, gl_entries, posting_date, provisional_account, reverse=0 self, item, gl_entries, posting_date, provisional_account, reverse=0
): ):
credit_currency = get_account_currency(provisional_account) credit_currency = get_account_currency(provisional_account)
debit_currency = get_account_currency(item.expense_account)
expense_account = item.expense_account expense_account = item.expense_account
debit_currency = get_account_currency(item.expense_account)
remarks = self.get("remarks") or _("Accounting Entry for Service") remarks = self.get("remarks") or _("Accounting Entry for Service")
multiplication_factor = 1 multiplication_factor = 1
@@ -631,11 +660,8 @@ class PurchaseReceipt(BuyingController):
) )
def make_tax_gl_entries(self, gl_entries): def make_tax_gl_entries(self, gl_entries):
if erpnext.is_perpetual_inventory_enabled(self.company):
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")]) negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get("items")])
is_asset_pr = any(d.is_fixed_asset for d in self.get("items"))
# Cost center-wise amount breakup for other charges included for valuation # Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {} valuation_tax = {}
for tax in self.get("taxes"): for tax in self.get("taxes"):
@@ -655,26 +681,26 @@ class PurchaseReceipt(BuyingController):
if negative_expense_to_be_booked and valuation_tax: if negative_expense_to_be_booked and valuation_tax:
# Backward compatibility: # Backward compatibility:
# If expenses_included_in_valuation account has been credited in against PI
# and charges added via Landed Cost Voucher, # and charges added via Landed Cost Voucher,
# post valuation related charges on "Stock Received But Not Billed" # post valuation related charges on "Stock Received But Not Billed"
# introduced in 2014 for backward compatibility of expenses already booked in expenses_included_in_valuation account
negative_expense_booked_in_pi = frappe.db.sql(
"""select name from `tabPurchase Invoice Item` pi
where docstatus = 1 and purchase_receipt=%s
and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
and voucher_no=pi.parent and account=%s)""",
(self.name, expenses_included_in_valuation),
)
against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0]) against_account = ", ".join([d.account for d in gl_entries if flt(d.debit) > 0])
total_valuation_amount = sum(valuation_tax.values()) total_valuation_amount = sum(valuation_tax.values())
amount_including_divisional_loss = negative_expense_to_be_booked amount_including_divisional_loss = negative_expense_to_be_booked
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = (
self.get("asset_received_but_not_billed")
if is_asset_pr
else self.get_company_default("stock_received_but_not_billed")
)
i = 1 i = 1
for tax in self.get("taxes"): for tax in self.get("taxes"):
if valuation_tax.get(tax.name): if valuation_tax.get(tax.name):
negative_expense_booked_in_pi = frappe.db.sql(
"""select name from `tabPurchase Invoice Item` pi
where docstatus = 1 and purchase_receipt=%s
and exists(select name from `tabGL Entry` where voucher_type='Purchase Invoice'
and voucher_no=pi.parent and account=%s)""",
(self.name, tax.account_head),
)
if negative_expense_booked_in_pi: if negative_expense_booked_in_pi:
account = stock_rbnb account = stock_rbnb
@@ -702,103 +728,6 @@ class PurchaseReceipt(BuyingController):
i += 1 i += 1
def get_asset_gl_entry(self, gl_entries):
for item in self.get("items"):
if item.is_fixed_asset:
if is_cwip_accounting_enabled(item.asset_category):
self.add_asset_gl_entries(item, gl_entries)
if flt(item.landed_cost_voucher_amount):
self.add_lcv_gl_entries(item, gl_entries)
# update assets gross amount by its valuation rate
# valuation rate is total of net rate, raw mat supp cost, tax amount, lcv amount per item
self.update_assets(item, item.valuation_rate)
return gl_entries
def add_asset_gl_entries(self, item, gl_entries):
arbnb_account = self.get_company_default("asset_received_but_not_billed")
# This returns category's cwip account if not then fallback to company's default cwip account
cwip_account = get_asset_account(
"capital_work_in_progress_account", asset_category=item.asset_category, company=self.company
)
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
remarks = self.get("remarks") or _("Accounting Entry for Asset")
cwip_account_currency = get_account_currency(cwip_account)
# debit cwip account
debit_in_account_currency = (
base_asset_amount if cwip_account_currency == self.company_currency else asset_amount
)
self.add_gl_entry(
gl_entries=gl_entries,
account=cwip_account,
cost_center=item.cost_center,
debit=base_asset_amount,
credit=0.0,
remarks=remarks,
against_account=arbnb_account,
debit_in_account_currency=debit_in_account_currency,
item=item,
)
asset_rbnb_currency = get_account_currency(arbnb_account)
# credit arbnb account
credit_in_account_currency = (
base_asset_amount if asset_rbnb_currency == self.company_currency else asset_amount
)
self.add_gl_entry(
gl_entries=gl_entries,
account=arbnb_account,
cost_center=item.cost_center,
debit=0.0,
credit=base_asset_amount,
remarks=remarks,
against_account=cwip_account,
credit_in_account_currency=credit_in_account_currency,
item=item,
)
def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default(
"expenses_included_in_asset_valuation"
)
if not is_cwip_accounting_enabled(item.asset_category):
asset_account = get_asset_category_account(
asset_category=item.asset_category, fieldname="fixed_asset_account", company=self.company
)
else:
# This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
remarks = self.get("remarks") or _("Accounting Entry for Stock")
self.add_gl_entry(
gl_entries=gl_entries,
account=expenses_included_in_asset_valuation,
cost_center=item.cost_center,
debit=0.0,
credit=flt(item.landed_cost_voucher_amount),
remarks=remarks,
against_account=asset_account,
project=item.project,
item=item,
)
self.add_gl_entry(
gl_entries=gl_entries,
account=asset_account,
cost_center=item.cost_center,
debit=flt(item.landed_cost_voucher_amount),
credit=0.0,
remarks=remarks,
against_account=expenses_included_in_asset_valuation,
project=item.project,
item=item,
)
def update_assets(self, item, valuation_rate): def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all( assets = frappe.db.get_all(
"Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code} "Asset", filters={"purchase_receipt": self.name, "item_code": item.item_code}
@@ -832,6 +761,20 @@ class PurchaseReceipt(BuyingController):
self.load_from_db() self.load_from_db()
def get_stock_value_difference(voucher_no, voucher_detail_no, warehouse):
return frappe.db.get_value(
"Stock Ledger Entry",
{
"voucher_type": "Purchase Receipt",
"voucher_no": voucher_no,
"voucher_detail_no": voucher_detail_no,
"warehouse": warehouse,
"is_cancelled": 0,
},
"stock_value_difference",
)
def update_billed_amount_based_on_po(po_details, update_modified=True): def update_billed_amount_based_on_po(po_details, update_modified=True):
po_billed_amt_details = get_billed_amount_against_po(po_details) po_billed_amt_details = get_billed_amount_against_po(po_details)

View File

@@ -928,17 +928,33 @@ class TestPurchaseReceipt(FrappeTestCase):
pr1.cancel() pr1.cancel()
def test_stock_transfer_from_purchase_receipt(self): def test_stock_transfer_from_purchase_receipt(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
customer = "_Test Internal Customer 2"
company = "_Test Company with perpetual inventory"
pr1 = make_purchase_receipt( pr1 = make_purchase_receipt(
warehouse="Work In Progress - TCP1", company="_Test Company with perpetual inventory" warehouse="Stores - TCP1", company="_Test Company with perpetual inventory"
) )
pr = make_purchase_receipt( dn1 = create_delivery_note(
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 item_code=pr1.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=5,
rate=500,
warehouse="Stores - TCP1",
target_warehouse="Work In Progress - TCP1",
) )
pr.supplier_warehouse = "" pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "Work In Progress - TCP1" pr.items[0].from_warehouse = "Work In Progress - TCP1"
pr.items[0].warehouse = "Stores - TCP1"
pr.submit() pr.submit()
gl_entries = get_gl_entries("Purchase Receipt", pr.name) gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -955,6 +971,11 @@ class TestPurchaseReceipt(FrappeTestCase):
pr1.cancel() pr1.cancel()
def test_stock_transfer_from_purchase_receipt_with_valuation(self): def test_stock_transfer_from_purchase_receipt_with_valuation(self):
from erpnext.stock.doctype.delivery_note.delivery_note import make_inter_company_purchase_receipt
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
prepare_data_for_internal_transfer()
create_warehouse( create_warehouse(
"_Test Warehouse for Valuation", "_Test Warehouse for Valuation",
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
@@ -962,16 +983,28 @@ class TestPurchaseReceipt(FrappeTestCase):
) )
pr1 = make_purchase_receipt( pr1 = make_purchase_receipt(
warehouse="_Test Warehouse for Valuation - TCP1", warehouse="Stores - TCP1",
company="_Test Company with perpetual inventory", company="_Test Company with perpetual inventory",
) )
pr = make_purchase_receipt( customer = "_Test Internal Customer 2"
company="_Test Company with perpetual inventory", warehouse="Stores - TCP1", do_not_save=1 company = "_Test Company with perpetual inventory"
dn1 = create_delivery_note(
item_code=pr1.items[0].item_code,
company=company,
customer=customer,
cost_center="Main - TCP1",
expense_account="Cost of Goods Sold - TCP1",
qty=5,
rate=50,
warehouse="Stores - TCP1",
target_warehouse="_Test Warehouse for Valuation - TCP1",
) )
pr = make_inter_company_purchase_receipt(dn1.name)
pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1" pr.items[0].from_warehouse = "_Test Warehouse for Valuation - TCP1"
pr.supplier_warehouse = "" pr.items[0].warehouse = "Stores - TCP1"
pr.append( pr.append(
"taxes", "taxes",
@@ -2203,6 +2236,21 @@ class TestPurchaseReceipt(FrappeTestCase):
for entry in gl_entries: for entry in gl_entries:
self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference)) self.assertEqual(abs(entry.debit + entry.credit), abs(sl_entries[0].stock_value_difference))
def non_internal_transfer_purchase_receipt(self):
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
pr_doc = make_purchase_receipt(do_not_submit=True)
warehouse = create_warehouse("Internal Transfer Warehouse", pr_doc.company)
pr_doc.items[0].db_set("target_warehouse", "warehouse")
pr_doc.reload()
self.assertEqual(pr_doc.items[0].from_warehouse, warehouse.name)
pr_doc.save()
pr_doc.reload()
self.assertFalse(pr_doc.items[0].from_warehouse)
def prepare_data_for_internal_transfer(): def prepare_data_for_internal_transfer():
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_internal_supplier

View File

@@ -118,7 +118,9 @@
"dimension_col_break", "dimension_col_break",
"cost_center", "cost_center",
"section_break_80", "section_break_80",
"page_break" "page_break",
"sales_order",
"sales_order_item"
], ],
"fields": [ "fields": [
{ {
@@ -1025,12 +1027,32 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "WIP Composite Asset", "label": "WIP Composite Asset",
"options": "Asset" "options": "Asset"
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"no_copy": 1,
"options": "Sales Order",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"hidden": 1,
"label": "Sales Order Item",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"search_index": 1
} }
], ],
"idx": 1, "idx": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-10-03 21:11:50.547261", "modified": "2023-10-19 10:50:58.071735",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Purchase Receipt Item", "name": "Purchase Receipt Item",

View File

@@ -451,31 +451,37 @@ class StockEntry(StockController):
item_code.append(item.item_code) item_code.append(item.item_code)
def validate_fg_completed_qty(self): def validate_fg_completed_qty(self):
item_wise_qty = {} if self.purpose != "Manufacture":
if self.purpose == "Manufacture" and self.work_order: return
for d in self.items:
if d.is_finished_item:
if self.process_loss_qty:
d.qty = self.fg_completed_qty - self.process_loss_qty
item_wise_qty.setdefault(d.item_code, []).append(d.qty) fg_qty = defaultdict(float)
for d in self.items:
if d.is_finished_item:
fg_qty[d.item_code] += flt(d.qty)
if not fg_qty:
return
precision = frappe.get_precision("Stock Entry Detail", "qty") precision = frappe.get_precision("Stock Entry Detail", "qty")
for item_code, qty_list in item_wise_qty.items(): fg_item = list(fg_qty.keys())[0]
total = flt(sum(qty_list), precision) fg_item_qty = flt(fg_qty[fg_item], precision)
fg_completed_qty = flt(self.fg_completed_qty, precision)
if (self.fg_completed_qty - total) > 0 and not self.process_loss_qty: for d in self.items:
self.process_loss_qty = flt(self.fg_completed_qty - total, precision) if not fg_qty.get(d.item_code):
self.process_loss_percentage = flt(self.process_loss_qty * 100 / self.fg_completed_qty) continue
if self.process_loss_qty: if (fg_completed_qty - fg_item_qty) > 0:
total += flt(self.process_loss_qty, precision) self.process_loss_qty = fg_completed_qty - fg_item_qty
if self.fg_completed_qty != total: if not self.process_loss_qty:
continue
if fg_completed_qty != (flt(fg_item_qty) + flt(self.process_loss_qty, precision)):
frappe.throw( frappe.throw(
_("The finished product {0} quantity {1} and For Quantity {2} cannot be different").format( _(
frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty) "Since there is a process loss of {0} units for the finished good {1}, you should reduce the quantity by {0} units for the finished good {1} in the Items Table."
) ).format(frappe.bold(self.process_loss_qty), frappe.bold(d.item_code))
) )
def validate_difference_account(self): def validate_difference_account(self):

View File

@@ -447,9 +447,7 @@ class TestStockEntry(FrappeTestCase):
repack.posting_date = nowdate() repack.posting_date = nowdate()
repack.posting_time = nowtime() repack.posting_time = nowtime()
expenses_included_in_valuation = frappe.get_value( default_expense_account = frappe.get_value("Company", company, "default_expense_account")
"Company", company, "expenses_included_in_valuation"
)
items = get_multiple_items() items = get_multiple_items()
repack.items = [] repack.items = []
@@ -460,12 +458,12 @@ class TestStockEntry(FrappeTestCase):
"additional_costs", "additional_costs",
[ [
{ {
"expense_account": expenses_included_in_valuation, "expense_account": default_expense_account,
"description": "Actual Operating Cost", "description": "Actual Operating Cost",
"amount": 1000, "amount": 1000,
}, },
{ {
"expense_account": expenses_included_in_valuation, "expense_account": default_expense_account,
"description": "Additional Operating Cost", "description": "Additional Operating Cost",
"amount": 200, "amount": 200,
}, },
@@ -504,9 +502,7 @@ class TestStockEntry(FrappeTestCase):
self.check_gl_entries( self.check_gl_entries(
"Stock Entry", "Stock Entry",
repack.name, repack.name,
sorted( sorted([[stock_in_hand_account, 1200, 0.0], ["Cost of Goods Sold - TCP1", 0.0, 1200.0]]),
[[stock_in_hand_account, 1200, 0.0], ["Expenses Included In Valuation - TCP1", 0.0, 1200.0]]
),
) )
def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle): def check_stock_ledger_entries(self, voucher_type, voucher_no, expected_sle):

View File

@@ -771,6 +771,12 @@ def get_default_cost_center(args, item=None, item_group=None, brand=None, compan
data = frappe.get_attr(path)(args.get("item_code"), company) data = frappe.get_attr(path)(args.get("item_code"), company)
if data and (data.selling_cost_center or data.buying_cost_center): if data and (data.selling_cost_center or data.buying_cost_center):
if args.get("customer") and data.selling_cost_center:
return data.selling_cost_center
elif args.get("supplier") and data.buying_cost_center:
return data.buying_cost_center
return data.selling_cost_center or data.buying_cost_center return data.selling_cost_center or data.buying_cost_center
if not cost_center and args.get("cost_center"): if not cost_center and args.get("cost_center"):

View File

@@ -299,7 +299,6 @@ class SubcontractingReceipt(SubcontractingController):
def make_item_gl_entries(self, gl_entries, warehouse_account=None): def make_item_gl_entries(self, gl_entries, warehouse_account=None):
stock_rbnb = self.get_company_default("stock_received_but_not_billed") stock_rbnb = self.get_company_default("stock_received_but_not_billed")
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
warehouse_with_no_account = [] warehouse_with_no_account = []
@@ -371,10 +370,7 @@ class SubcontractingReceipt(SubcontractingController):
divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount")) divisional_loss = flt(item.amount - stock_value_diff, item.precision("amount"))
if divisional_loss: if divisional_loss:
if self.is_return: loss_account = item.expense_account
loss_account = expenses_included_in_valuation
else:
loss_account = item.expense_account
self.add_gl_entry( self.add_gl_entry(
gl_entries=gl_entries, gl_entries=gl_entries,

View File

@@ -369,7 +369,7 @@ Base,Basis,
Base URL,Basis-URL, Base URL,Basis-URL,
Based On,Basiert auf, Based On,Basiert auf,
Based On Payment Terms,Basierend auf Zahlungsbedingungen, Based On Payment Terms,Basierend auf Zahlungsbedingungen,
Basic,Grundeinkommen, Basic,Basic,
Batch,Charge, Batch,Charge,
Batch Entries,Batch-Einträge, Batch Entries,Batch-Einträge,
Batch ID is mandatory,Batch-ID ist obligatorisch, Batch ID is mandatory,Batch-ID ist obligatorisch,
Can't render this file because it is too large.